Skip to content

Conversation

@ajara87
Copy link
Member

@ajara87 ajara87 commented Nov 26, 2025

Porting neo-project/neo#4286 of @adrian-fjellberg

  • Added use of GetSignData

  • Added UTs

    • TestOnSignMessageCommand
    • TestOnSignMessageCommandWithoutPassword
    • TestOnSignMessageCommandWrongPassword
    • TestOnSignMessageCommandWithoutAccount

@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

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.

RandomNumberGenerator.Fill(saltBytes);
var saltHex = saltBytes.ToHexString().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.

I think we should remove this salt. Perhaps we should have an integer (4 bytes) for ValidUntilBlock, but I don't see why a salt would be useful here

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants