Skip to content

port message sign to master-n3#924

Open
ajara87 wants to merge 33 commits intoneo-project:master-n3from
ajara87:message-sign
Open

port message sign to master-n3#924
ajara87 wants to merge 33 commits intoneo-project:master-n3from
ajara87:message-sign

Conversation

@ajara87
Copy link
Member

@ajara87 ajara87 commented Nov 26, 2025

Porting neo-project/neo#4286 of @adrian-fjellberg
Proposal neo-project/proposals#213

  • Added use of GetSignData

  • Added UTs

    • TestOnSignMessageCommand
    • TestOnSignMessageCommandWithoutPassword
    • TestOnSignMessageCommandWrongPassword
    • TestOnSignMessageCommandWithoutAccount
    • TestOnVerifyMessageCommand

@ajara87 ajara87 marked this pull request as draft November 26, 2025 06:54
@ajara87 ajara87 mentioned this pull request Nov 27, 2025
@ajara87
Copy link
Member Author

ajara87 commented Nov 27, 2025

Need #926 for UT

@github-actions github-actions bot added the N3 label Nov 27, 2025
@Jim8y
Copy link
Contributor

Jim8y commented Dec 3, 2025

Need #926 for UT

@ajara87 926 is merged

@ajara87
Copy link
Member Author

ajara87 commented Dec 3, 2025

Need #926 for UT

@ajara87 926 is merged

thank you @Jim8y. I have to upload a proposal and the PR is ready to review.

cschuchardt88
cschuchardt88 previously approved these changes Dec 3, 2025
@shargon
Copy link
Member

shargon commented Dec 3, 2025

@cschuchardt88 It's a draft

@ajara87 ajara87 marked this pull request as ready for review December 8, 2025 21:35
@superboyiii
Copy link
Member

superboyiii commented Dec 9, 2025

Message to sign -> Hello World!
CLI command: sign message "Hello world!"
Result:

Signed Payload:
010001f02e36323164323265666630626137333735666362363437363530366634316563362248656c6c6f20776f726c6421220000

    Curve: secp256r1
Algorithm: payload = 010001f0 + VarBytes(Salt + Message) + 0000
Algorithm: Sign(SHA256(network || Hash256(payload)))
           See the online documentation for details on how to verify this signature.
           https://developers.neo.org/docs/n3/node/cli/cli#sign_message

Message used for signing: ""Hello world!""
Message bytes: 2248656c6c6f20776f726c642122

Generated signatures:

    Address: NezDaziDDyWCnSqTVhSrLCxzVZKyVb3qmo
  PublicKey: 021c37a423b5885c4320d501128e1b502e86b40ce1c0dbc9d2f20ba98924228ef9
  Signature: 733abd56cc64154c036648c59527c4013d60b9f6caca33a748c64cb5b8f7f9b1716d1224401939c85c34d689b0d6860655cb9b78999a0eabffee83954b70cabb
       Salt: 621d22eff0ba7375fcb6476506f41ec6

2248656c6c6f20776f726c642122 hex to string is: "Hello world!". This is not good. In most CLI command, "" just means it's string, no other meaning, but sign message makes it combined with salt direclty, people may wrongly input this "".

@superboyiii
Copy link
Member

We need add verify message as well.
For example:

    /// <summary>
    /// Process "verify message" command
    /// </summary>
    /// <param name="message">Original message that was signed</param>
    /// <param name="signature">Signature in hex format</param>
    /// <param name="publicKey">Public key in hex format</param>
    /// <param name="salt">Salt in hex format</param>
    [ConsoleCommand("verify message", Category = "Wallet Commands")]
    private void OnVerifyMessageCommand(string message, string signature, string publicKey, string salt)
    {
        try
        {
            if (message.Length >= 2)
            {
                if ((message[0] == '"' && message[^1] == '"') || (message[0] == '\'' && message[^1] == '\''))
                {
                    message = message[1..^1];
                }
            }

            // Parse public key
            if (!ECPoint.TryParse(publicKey, ECCurve.Secp256r1, out var pubKey))
            {
                ConsoleHelper.Error("Invalid public key format");
                return;
            }

            // Parse signature
            byte[] signatureBytes;
            try
            {
                signatureBytes = signature.HexToBytes();
            }
            catch
            {
                ConsoleHelper.Error("Invalid signature format (must be hex string)");
                return;
            }

            // Validate salt format (should be hex string, typically 32 characters for 16 bytes)
            if (string.IsNullOrEmpty(salt))
            {
                ConsoleHelper.Error("Salt cannot be empty");
                return;
            }

            // Reconstruct payload: 010001f0 + VarBytes(Salt + Message) + 0000
            // Note: salt is used as hex string (lowercase), same as in signing
            var saltHex = salt.ToLowerInvariant();
            var paramBytes = Encoding.UTF8.GetBytes(saltHex + message);
            byte[] payload;
            using (var ms = new MemoryStream())
            using (var w = new BinaryWriter(ms, Encoding.UTF8, true))
            {
                w.Write((byte)0x01);
                w.Write((byte)0x00);
                w.Write((byte)0x01);
                w.Write((byte)0xF0);
                w.WriteVarBytes(paramBytes);
                w.Write((ushort)0);
                w.Flush();
                payload = ms.ToArray();
            }

            // Calculate signData: SHA256(network || Hash256(payload))
            var hash = new UInt256(Crypto.Hash256(payload));
            var signData = hash.GetSignData(NeoSystem.Settings.Network);
            bool isValid = Crypto.VerifySignature(signData, signatureBytes, pubKey);
            var contract = Contract.CreateSignatureContract(pubKey);
            var address = contract.ScriptHash.ToAddress(NeoSystem.Settings.AddressVersion);

            Console.WriteLine();
            ConsoleHelper.Info("Verification Result:");
            Console.WriteLine();
            ConsoleHelper.Info("    Address: ", address);
            ConsoleHelper.Info("  PublicKey: ", pubKey.EncodePoint(true).ToHexString());
            ConsoleHelper.Info("  Signature: ", signature);
            ConsoleHelper.Info("       Salt: ", saltHex);
            ConsoleHelper.Info("     Status: ", isValid ? "Valid" : "Invalid");
            Console.WriteLine();

            if (!isValid)
            {
                ConsoleHelper.Info("Debug Information:");
                Console.WriteLine();
                ConsoleHelper.Info("  Message used: ", $"\"{message}\"");
                ConsoleHelper.Info("  Message bytes: ", Encoding.UTF8.GetBytes(message).ToHexString());
                ConsoleHelper.Info("  Salt+Message: ", $"{saltHex}{message}");
                ConsoleHelper.Info("  Reconstructed Payload: ", payload.ToHexString());
                ConsoleHelper.Info("  Payload Hash256: ", hash.ToString());
                Console.WriteLine();
                ConsoleHelper.Warning("Note: The message must match exactly what was signed.");
                ConsoleHelper.Warning("If you used 'sign message \"Hello world!\"', the actual message signed is 'Hello world!' (without quotes).");
                ConsoleHelper.Warning("If the signed message contains quote characters, you need to include them in the verify command.");
                Console.WriteLine();
            }
        }
        catch (Exception e)
        {
            ConsoleHelper.Error($"Verification failed: {GetExceptionMessage(e)}");
        }
    }

@superboyiii
Copy link
Member

superboyiii commented Dec 10, 2025

And we need rpc sign message & verify message as well. It can be more useful in actual scenarios, especially rpc verify message.

Co-authored-by: Owen <38493437+superboyiii@users.noreply.github.com>
// Reconstruct payload: 010001f0 + VarBytes(Salt + Message) + 0000
// Note: salt is used as hex string (lowercase), same as in signing
var saltHex = salt.ToLowerInvariant();
var paramBytes = Encoding.UTF8.GetBytes(saltHex + message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message can contain the salt. I think we can remove the salt, and recommend to add it in the NEP

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roman-khimov what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is 010001f0?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roman-khimov what do you think?

Same as previous, this code just does what wallets were doing for years. They salted the message, good for them, OK for me.

What is 010001f0?

Magic. Chosen years ago to ensure that signed message is never a transaction. Still works for N3 since we don't have transaction version 1 (yet).

Refs:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compared to salt, I prefer adding a timestamp. Additionally, the timestamp, message content, etc., can be encapsulated into structured data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should not bother it so much~~~ as long as it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roman-khimov the thing is that we added SignData call, this add a signature protection that makes incompatible with the previous versions

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But who needs an incompatible version of this thing? As noticed previously, even NeoFS is compatible with this scheme:
https://github.com/nspcc-dev/neofs-sdk-go/blob/23f5f0a8f3de952108d36cdbe589d0a2910ce8be/crypto/ecdsa/wallet_connect.go#L108

because when we played with gateways/wallets it was quickly discovered that they do not allow signing arbitrary data, they can sign messages in this format. So we adapted to it with this additional signature scheme. I don't see why node wallet can't do the same. There is nothing inherently wrong here, mostly it depends on what is signed and how the message signed is used, but that's not a wallet responsibility.

cschuchardt88
cschuchardt88 previously approved these changes Jan 17, 2026
@Jim8y
Copy link
Contributor

Jim8y commented Jan 19, 2026

@shargon please add this one to 3.9.

@Jim8y Jim8y requested a review from shargon January 19, 2026 06:23
Jim8y
Jim8y previously approved these changes Jan 19, 2026
@superboyiii
Copy link
Member

@shargon please add this one to 3.9.

neo-project/neo#4091 (comment)
It's not good to add anything now. 3.9.0 will be released soon, we don't need be so hurry. We can add it to v3.9.1.

@superboyiii
Copy link
Member

superboyiii commented Jan 19, 2026

@ajara87 We need rpc signmsg and verifymsg in RpcServer as well. But can be made in another PR.

@ajara87
Copy link
Member Author

ajara87 commented Jan 19, 2026

@ajara87 We need rpc signmsg and verifymsg in RpcServer as well. But can be made in another PR.

Sure! I'll do It in another PR

@coveralls
Copy link

coveralls commented Jan 21, 2026

Pull Request Test Coverage Report for Build 21714050411

Details

  • 129 of 133 (96.99%) changed or added relevant lines in 2 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.5%) to 39.372%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/Neo.CLI/CLI/MainService.Wallet.cs 127 131 96.95%
Totals Coverage Status
Change from base Build 21707655558: 0.5%
Covered Lines: 7258
Relevant Lines: 17084

💛 - Coveralls

@superboyiii
Copy link
Member

Tested OK.


// Calculate signData: SHA256(network || Hash256(payload))
var hash = new UInt256(Crypto.Hash256(payload));
var signData = hash.GetSignData(NeoSystem.Settings.Network);
Copy link
Member

@shargon shargon Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already makes incompatible with the wallets version. I don't see that these wallets or providers have previously tried to make their "standard" official (by nep o github issue), so I don't understand why we have to respect a project's own version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants