-
Notifications
You must be signed in to change notification settings - Fork 223
port message sign to master-n3 #924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master-n3
Are you sure you want to change the base?
Changes from 33 commits
cd219c6
c57e6e7
f050e7a
5bfdbff
9520277
18bf56a
62d9cc3
995577f
87b6864
d178f20
78b9598
295ccd8
58836bb
5fdeeec
cd98183
f7e9137
d6babdb
3358226
eec7794
105c3cc
156dd25
4c7fb8d
b004143
5ab8d72
51190c8
53643e4
929a81b
6abda71
205211d
0b66999
e35806c
beefb8b
91c7449
0045632
708d8aa
d8ad5df
8f1c586
5f66021
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,8 +11,11 @@ | |
|
|
||
| using Akka.Actor; | ||
| using Neo.ConsoleService; | ||
| using Neo.Cryptography; | ||
| using Neo.Extensions; | ||
| using Neo.Extensions.Factories; | ||
| using Neo.Json; | ||
| using Neo.Network.P2P; | ||
| using Neo.Network.P2P.Payloads; | ||
| using Neo.Persistence; | ||
| using Neo.Sign; | ||
|
|
@@ -23,7 +26,9 @@ | |
| using Neo.Wallets.NEP6; | ||
| using System.Numerics; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using static Neo.SmartContract.Helper; | ||
| using ECCurve = Neo.Cryptography.ECC.ECCurve; | ||
| using ECPoint = Neo.Cryptography.ECC.ECPoint; | ||
|
|
||
| namespace Neo.CLI; | ||
|
|
@@ -470,8 +475,8 @@ private void OnListKeyCommand() | |
| /// <summary> | ||
| /// Process "sign" command | ||
| /// </summary> | ||
| /// <param name="jsonObjectToSign">Json object to sign</param> | ||
| [ConsoleCommand("sign", Category = "Wallet Commands")] | ||
| /// <param name="jsonObjectToSign">The json string that records the transaction information</param> | ||
| [ConsoleCommand("sign transaction", Category = "Wallet Commands")] | ||
| private void OnSignCommand(JObject jsonObjectToSign) | ||
| { | ||
| if (NoWallet()) return; | ||
|
|
@@ -503,6 +508,183 @@ private void OnSignCommand(JObject jsonObjectToSign) | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Process "sign message" command | ||
| /// </summary> | ||
| /// <param name="message">Message to sign</param> | ||
| [ConsoleCommand("sign message", Category = "Wallet Commands")] | ||
| private void OnSignMessageCommand(string message) | ||
| { | ||
| if (NoWallet()) return; | ||
|
|
||
| message = NormalizeMessage(message); | ||
|
|
||
| string password = ReadUserInput("password", true); | ||
| if (password.Length == 0) | ||
| { | ||
| ConsoleHelper.Info("Cancelled"); | ||
| return; | ||
| } | ||
|
|
||
| if (!CurrentWallet!.VerifyPassword(password)) | ||
| { | ||
| ConsoleHelper.Error("Incorrect password"); | ||
| return; | ||
| } | ||
|
|
||
ajara87 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (message == null) | ||
| { | ||
| ConsoleHelper.Error("Null message"); | ||
| return; | ||
| } | ||
|
|
||
| var saltBytes = new byte[16]; | ||
| saltBytes = RandomNumberFactory.NextBytes(saltBytes.Length, cryptography: true); | ||
| var saltHex = Convert.ToHexStringLower(saltBytes); | ||
|
|
||
| var paramBytes = Encoding.UTF8.GetBytes(saltHex + message); | ||
shargon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| byte[] payload; | ||
| using (var ms = new MemoryStream()) | ||
| using (var w = new BinaryWriter(ms, Encoding.UTF8, true)) | ||
| { | ||
| // We add these 4 bytes to prevent the signature from being a valid transaction | ||
| w.Write((byte)0x01); | ||
shargon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| w.Write((byte)0x00); | ||
| w.Write((byte)0x01); | ||
| w.Write((byte)0xF0); | ||
| // Write the actual message to sign | ||
| w.WriteVarBytes(paramBytes); | ||
| // We add these 2 bytes to prevent the signature from being a valid transaction | ||
| w.Write((ushort)0); | ||
| w.Flush(); | ||
| payload = ms.ToArray(); | ||
| } | ||
|
|
||
| ConsoleHelper.Info("Signed Payload: ", $"{payload.ToHexString()}"); | ||
| Console.WriteLine(); | ||
| ConsoleHelper.Info(" Curve: ", "secp256r1"); | ||
| ConsoleHelper.Info("Algorithm: ", "payload = 010001f0 + VarBytes(Salt + Message) + 0000"); | ||
| ConsoleHelper.Info("Algorithm: ", "Sign(SHA256(network || Hash256(payload)))"); | ||
| ConsoleHelper.Info(" ", "See the online documentation for details on how to verify this signature."); | ||
| ConsoleHelper.Info(" ", "https://developers.neo.org/docs/n3/node/cli/cli#sign_message"); | ||
| Console.WriteLine(); | ||
| ConsoleHelper.Info("Generated signatures:"); | ||
| Console.WriteLine(); | ||
|
|
||
| var hash = new UInt256(Crypto.Hash256(payload)); | ||
| var signData = hash.GetSignData(NeoSystem.Settings.Network); | ||
|
|
||
| foreach (WalletAccount account in CurrentWallet.GetAccounts().Where(p => p.HasKey)) | ||
| { | ||
| var key = account.GetKey(); | ||
| var signature = Crypto.Sign(signData, key!.PrivateKey, ECCurve.Secp256r1); | ||
|
|
||
| ConsoleHelper.Info(" Address: ", account.Address); | ||
| ConsoleHelper.Info(" PublicKey: ", key.PublicKey.EncodePoint(true).ToHexString()); | ||
| ConsoleHelper.Info(" Signature: ", signature.ToHexString()); | ||
| ConsoleHelper.Info(" Salt: ", saltHex); | ||
| Console.WriteLine(); | ||
| } | ||
| } | ||
|
|
||
| /// <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 | ||
| { | ||
| message = NormalizeMessage(message); | ||
|
|
||
| // 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @roman-khimov what do you think?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Same as previous, this code just does what wallets were doing for years. They salted the message, good for them, OK for me.
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:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @roman-khimov the thing is that we added There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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. |
||
| 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)}"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Process "send" command | ||
| /// </summary> | ||
|
|
@@ -735,7 +917,18 @@ private void OnChangePasswordCommand() | |
| ConsoleHelper.Error("Failed to change password"); | ||
| } | ||
| } | ||
| private string NormalizeMessage(string message) | ||
| { | ||
| if (string.IsNullOrEmpty(message) || message.Length < 2) return message; | ||
|
|
||
| var first = message[0]; | ||
| var last = message[^1]; | ||
|
|
||
| if (first == last && (first == '"' || first == '\'')) | ||
| return message[1..^1]; | ||
|
|
||
| return message; | ||
| } | ||
| private void SignAndSendTx(DataCache snapshot, Transaction tx) | ||
| { | ||
| if (NoWallet()) return; | ||
|
|
@@ -762,4 +955,5 @@ private void SignAndSendTx(DataCache snapshot, Transaction tx) | |
| ConsoleHelper.Info("Incomplete signature:\n", $"{context}"); | ||
| } | ||
| } | ||
| internal Func<string, bool, string> ReadUserInput { get; set; } = ConsoleHelper.ReadUserInput; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Copyright (C) 2015-2026 The Neo Project. | ||
| // | ||
| // TestUtils.cs file belongs to the neo project and is free | ||
| // software distributed under the MIT software license, see the | ||
| // accompanying file LICENSE in the main directory of the | ||
| // repository or http://www.opensource.org/licenses/mit-license.php | ||
| // for more details. | ||
| // | ||
| // Redistribution and use in source and binary forms with or without | ||
| // modifications are permitted. | ||
|
|
||
| using Neo.Json; | ||
| using Neo.Wallets.NEP6; | ||
|
|
||
| namespace Neo.CLI.Tests; | ||
|
|
||
| public static partial class TestUtils | ||
| { | ||
| public static NEP6Wallet GenerateTestWallet(string password) | ||
| { | ||
| var wallet = new JObject() | ||
| { | ||
| ["name"] = "noname", | ||
| ["version"] = new Version("1.0").ToString(), | ||
| ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), | ||
| ["accounts"] = new JArray(), | ||
| ["extra"] = null | ||
| }; | ||
| Assert.AreEqual("{\"name\":\"noname\",\"version\":\"1.0\",\"scrypt\":{\"n\":2,\"r\":1,\"p\":1},\"accounts\":[],\"extra\":null}", wallet.ToString()); | ||
| return new NEP6Wallet(null, password, TestProtocolSettings.Default, wallet); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.