pyca / pynacl

Python binding to the Networking and Cryptography (NaCl) library
https://pynacl.readthedocs.io/
Apache License 2.0
1.05k stars 231 forks source link

Fails to verify JS signed messages #737

Closed AnderUstarroz closed 2 years ago

AnderUstarroz commented 2 years ago

Describe the bug Javascript Signed messages cannot be verified using pynacl (Python)

To Reproduce Sign 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, world!');
            // 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;
};

Try to verify the signed message using PyNaCl and Solana-py:

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

result = VerifyKey(bytes(PublicKey("HERE_THE_PUB_KEY"))
  ).verify(
    smessage=bytes("HERE_THE_MESSAGE", "utf8"),
    signature=bytes("HERE_THE_SIGNATURE", "utf8"),
)

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

Expected behavior The verification should be successful, same way as when using sign.detached.verify() in JS.

Actual behavior The verification throws: nacl.exceptions.BadSignatureError: Signature was forged or corrupt.

Maybe it has something to do with the encoding? In JS the bytes have the following types:

Pubkey:  Uint8Array(32) [144, 188, 240, ...
Message: Uint8Array(44) [57,   85,  65, ...
Signed:  Uint8Array(64) [82,  220, 242, ...

And in python:

Pubkey:  <class 'bytes'> b'\x90\xbc\xf...'
Message: <class 'bytes'> b'9UA...'
Signed:  <class 'bytes'> b'2f68j5...'
Hungryee commented 2 years ago

^^ Interested in this too PyNacl and Tweetnacl seem to be incompatible

reaperhulk commented 2 years ago

PyNaCl uses libsodium and passes all tests. I've never used tweetnacl but I would expect it, too, is well tested so it's likely that there's a bug in the way people are calling one or both. As a quick test I went to https://tweetnacl.js.org/#/sign and generated a random secret key (N4BHr448pwlfZF26vOg8BNJWuQHt1RZD+NRIk3Yt5HwFTUE6y9EKtMmyHr4jUEc6fWPW0d8bpFRvbQbyQ2EtuQ==), put a message testing, and got the signature 0qUSNxiDKrDGiwhph4fSdPblWEhTvawSKsvqCK4tDJJ8ESPw5oOrQ3h06IsYtYauokPCTM8k4/bllslUiCzmCg==. I then verified it on the pynacl side with

from nacl.signing import VerifyKey

VerifyKey(base64.b64decode(b"BU1BOsvRCrTJsh6+I1BHOn1j1tHfG6RUb20G8kNhLbk=")).verify(smessage=b"testing", signature=base64.b64decode("0qUSNxiDKrDGiwhph4fSdPblWEhTvawSKsvqCK4tDJJ8ESPw5oOrQ3h06IsYtYauokPCTM8k4/bllslUiCzmCg=="))

Which worked without issue. Note that you need to decode the base64 as PyNaCl expects bytes, not encoded data. I suspect the failures here are related to encoding of the signature.