neo-node icon indicating copy to clipboard operation
neo-node copied to clipboard

port message sign to master-n3

Open ajara87 opened this issue 3 months ago • 7 comments

Porting https://github.com/neo-project/neo/pull/4286 of @adrian-fjellberg

  • Added use of GetSignData

  • Added UTs

    • TestOnSignMessageCommand
    • TestOnSignMessageCommandWithoutPassword
    • TestOnSignMessageCommandWrongPassword
    • TestOnSignMessageCommandWithoutAccount

ajara87 avatar Nov 26 '25 06:11 ajara87

Need #926 for UT

ajara87 avatar Nov 27 '25 13:11 ajara87

Need https://github.com/neo-project/neo-node/pull/926 for UT

@ajara87 926 is merged

Jim8y avatar Dec 03 '25 03:12 Jim8y

Need #926 for UT

@ajara87 926 is merged

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

ajara87 avatar Dec 03 '25 07:12 ajara87

@cschuchardt88 It's a draft

shargon avatar Dec 03 '25 09:12 shargon

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 avatar Dec 09 '25 08:12 superboyiii

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 avatar Dec 10 '25 08:12 superboyiii

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

superboyiii avatar Dec 10 '25 08:12 superboyiii