11

Having created a signed message I'm unsure how to use the resulting signature to verify the message using the publicKey.

My use case is, I'm wanting to use a Solana Wallet to login to an API server with a pattern like:

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

I've attempted to use nodeJS crypto.verify to decode the signed message on the API side but am a bit out of my depth digging into Buffers and elliptic curves:

// 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);

I'm pretty sure I'm going about this the wrong way and there must be an obvious method I'm missing.

As the space matures I expect a verify function or lib will appear to to consume the output of const signed = await wallet.sign(data, "hex");

Something like:

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

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

But after 3 days of pushing hard I'm starting to hit my limits and my brain is failing. Any help or direction where to look much appreciated

harkl
  • 872
  • 7
  • 17
  • Did you try using Solana's [web3 API](https://solana-labs.github.io/solana-web3.js/classes/transaction.html#verifysignatures) instead ? – Standaa - Remember Monica Jul 08 '21 at 13:40
  • Also, did you try to verify it using the signing example [provided here](https://github.com/project-serum/sol-wallet-adapter#sign-a-message) ? – Standaa - Remember Monica Jul 08 '21 at 13:49
  • ```const data = new TextEncoder().encode('message to verify'); const signed = await wallet.sign(data, "hex"); ... signature: toHexString(signed.signature),``` You're using hex here but serum example shows UTF8 – Chase Barker Jul 08 '21 at 17:30
  • Also the verification method is questionable. ```const verified = crypto.verify( ALGORITHM, nonceUint8, `-----BEGIN PUBLIC KEY-----\n${user.publicAddress}\n-----END PUBLIC KEY-----`, signature );``` a public address and a public key aren't the same. And assuming user.publicAddress is even correct, is it in the right format, e.g. base64? – Chase Barker Jul 08 '21 at 17:31
  • Looking at the ETH web3 docs is probably the best way to describe what is trying to be achieved here. The equivalent of the `sign` function is what I'm looking at here https://web3js.readthedocs.io/en/v1.2.9/web3-eth-personal.html?highlight=verify%20message#sign. The distinction being that signing and recovering a message doesn't incur an on chain transaction. You can see the corresponding `ecRecover` function here https://web3js.readthedocs.io/en/v1.2.11/web3-eth-personal.html#ecrecover. You'll also notice that these functions are distinct from `signTransaction` and `sendTransaction`. – harkl Jul 08 '21 at 21:57
  • Check an working example https://github.com/enginer/solana-message-sign-verify-example – Enginer Jul 12 '22 at 22:02

5 Answers5

12

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 tweetnacl 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
harkl
  • 872
  • 7
  • 17
  • 2
    great! make sure to accept the correct answer here on SO so others know =) – Chase Barker Jul 09 '21 at 01:17
  • 1
    This saved my life ! Thank you ! I tried for a whole day to do this in PHP but gave up on it, so I had to spin up a small node.js docker that will do only this one thing .... sucks.. does anyone know how to do this in PHP ? – Gotys Jan 31 '22 at 05:50
8

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 nacl.sign.detached.verify

in @solana/web3.js https://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.

Arowana
  • 193
  • 4
1

For iOS, solana.request will cause error. Use solana.signMessage and base58 encode the signature.

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);
}
ouflak
  • 2,458
  • 10
  • 44
  • 49
OO Q
  • 11
  • 1
0

I needed to convert Uint8Array to string and convert it back to Uint8Array for HTTP communication. I found the toLocaleString method of Uint8Array helpful in this case. It outputs comma-separated integers as a string.

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

enter image description here

And then you can convert it back to Uint8Array with the following code.

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

Edit

The solution above was working on the desktop but when I tried my code inside the Phantom wallet iOS browser it gave an error. I guess the toLocaleString method is not available in that browser. I found a more solid solution to convert Uint8Array to a comma-separated string

Array.apply([], signedMessage.signature).join(",")
Farid Movsumov
  • 12,350
  • 8
  • 71
  • 97
0

Signing and base64 encode:

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

Base64 decode and verifying:

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

Full code example: https://github.com/enginer/solana-message-sign-verify-example

Enginer
  • 3,048
  • 1
  • 26
  • 22
  • 1
    `atob` function is deprecated. For code running using Node.js APIs, converting between base64-encoded strings and binary data should be performed using `Buffer.from(str, 'base64')` and `buf.toString('base64')` – Eliezer Steinbock Nov 29 '22 at 06:51