solanapynaclsolana-web3jssolana-program-library

How to verify with Python (PyNaCl) a message signed by Solana wallet adapter (javascript)


I have signed a message using Solana's wallet adapter example:

import { useWallet } from '@solana/wallet-adapter-react';
import bs58 from 'bs58';
import React, { FC, useCallback } from 'react';
import { sign } from 'tweetnacl';

export const SignMessageButton: FC = () => {
    const { publicKey, signMessage } = useWallet();

    const onClick = useCallback(async () => {
        try {
            // `publicKey` will be null if the wallet isn't connected
            if (!publicKey) throw new Error('Wallet not connected!');
            // `signMessage` will be undefined if the wallet doesn't support it
            if (!signMessage) throw new Error('Wallet does not support message signing!');

            // Encode anything as bytes
            const message = new TextEncoder().encode("hello");
            // Sign the bytes using the wallet
            const signature = await signMessage(message);
            // Verify that the bytes were signed using the private key that matches the known public key
            if (!sign.detached.verify(message, signature, publicKey.toBytes())) throw new Error('Invalid signature!');

            alert(`Message signature: ${bs58.encode(signature)}`);
        } catch (error: any) {
            alert(`Signing failed: ${error?.message}`);
        }
    }, [publicKey, signMessage]);

    return signMessage ? (<button onClick={onClick} disabled={!publicKey}>Sign Message</button>) : null;
};

But I am not being able to verify the signed message using Python 3.9 with PyNaCl and Solana-py. I have tried the following:

from nacl.signing import VerifyKey
from solana.publickey import PublicKey
import base58

pubkey = bytes(PublicKey("DKpHyR1WjWE23E3xizPUhefZKmpMrMXNBVfoxQ7WXCRR"))
msg = bytes("hello", 'utf8')
signed = bytes("3EWDdtU1w8pWkr6fg8faJvKn1wBZmNjgf5kUx4Pn5gw4HeBPYVDm7cTHNqpRVMami6yX36jdaeZacv9GXR19Jzye", 'utf8')

result = VerifyKey(
    pubkey
).verify(
    smessage=base58.b58decode(msg),
    signature=base58.b58decode(signed)
)

But verification returns: nacl.exceptions.BadSignatureError: Signature was forged or corrupt.

Somebody knows what is wrong? Could it be an encoding problem? Seems like JS uses the following byte types:

pubkey:  Uint8Array(32) [144, 188, 240, 167, 187, 75, 30, 17, 232, 175, 91, 222, 73, 68, 183, 218, 108, 56, 249, 64, 250, 61, 111, 168, 194, 233, 159, 2, 247, 5, 175, 124, buffer: ArrayBuffer(32), byteLength: 32, byteOffset: 0, length: 32]
message: Uint8Array(44) [57, 85, 65, 81, 76, 53, 81, 68, 67, 89, 122, 70, 112, 107, 119, 70, 88, 52, 88, 75, 53, 70, 119, 107, 66, 54, 67, 57, 116, 57, 116, 120, 65, 89, 52, 102, 102, 122, 69, 52, 114, 97, 113, 84, buffer: ArrayBuffer(44), byteLength: 44, byteOffset: 0, length: 44]
signed:  Uint8Array(64) [111, 173, 219, 10, 169, 113, 163, 35, 30, 162, 250, 243, 191, 106, 195, 99, 238, 34, 49, 192, 19, 92, 111, 142, 57, 31, 158, 235, 65, 219, 146, 176, 174, 48, 30, 255, 160, 90, 174, 179, 219, 197, 252, 189, 150, 225, 160, 133, 163, 109, 159, 80, 56, 191, 11, 1, 91, 111, 196, 214, 231, 84, 11, 1, buffer: ArrayBuffer(64), byteLength: 64, byteOffset: 0, length: 64]

And in python:

pubkey: b'\xb7\x1e+\xef\xe19#y}\xa4L\xf2K\rK\xc3\xbby\x93\x1c\x00L\xe1<\x19g`-\x9d\xd5\xee\x94'
msg:    b'Cn8eVZg'
signed: b'3EWDdtU1w8pWkr6fg8faJvKn1wBZmNjgf5kUx4Pn5gw4HeBPYVDm7cTHNqpRVMami6yX36jdaeZacv9GXR19Jzye'

Do I need to use some different encoding on Python?


Solution

  • Thanks for providing a concrete example on this, you're very close! The encoding is absolutely the issue here -- the pubkey is correctly encoded in Python as bytes. That first byte of \x90, encoded as two hex values, is 144 in JS, and you can check that in Python with: int('90', 16) = 144.

    So to verify your key, you can instead use the base58 package https://github.com/keis/base58 and do:

    from nacl.signing import VerifyKey
    from solana.publickey import PublicKey
    import base58
    
    pubkey = bytes(PublicKey("DKpHyR1WjWE23E3xizPUhefZKmpMrMXNBVfoxQ7WXCRR"))
    msg = bytes("hello", 'utf8')
    signed = bytes("3EWDdtU1w8pWkr6fg8faJvKn1wBZmNjgf5kUx4Pn5gw4HeBPYVDm7cTHNqpRVMami6yX36jdaeZacv9GXR19Jzye", 'utf8')
    
    result = VerifyKey(
        pubkey
    ).verify(
        smessage=msg,  
        signature=base58.b58decode(signed)
    )
    

    Note.- on smessage you don't need to use b58, because it was encoded with new TextEncoder().encode("hello").

    Second option: if you already have the UInt8Array from JS, you can do:

    result = VerifyKey(bytes(PublicKey("HERE_THE_PUB_KEY"))
      ).verify(
        smessage=bytes([byte1, byte2, byte3, ...])
        signature=bytes([byte1, byte2, byte3, ...])
    )