port message sign to master-n3
Porting https://github.com/neo-project/neo/pull/4286 of @adrian-fjellberg
-
Added use of GetSignData
-
Added UTs
- TestOnSignMessageCommand
- TestOnSignMessageCommandWithoutPassword
- TestOnSignMessageCommandWrongPassword
- TestOnSignMessageCommandWithoutAccount
Need #926 for UT
Need https://github.com/neo-project/neo-node/pull/926 for UT
@ajara87 926 is merged
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 It's a draft
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 "".
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)}");
}
}
And we need rpc sign message & verify message as well. It can be more useful in actual scenarios, especially rpc verify message.