Skip to content
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

[Core Add] Add support to Ed25519 #3507

Open
wants to merge 24 commits into
base: HF_Echidna
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/Neo/Cryptography/Ed25519.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// Ed25519.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 Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
using System;
using System.Linq;

namespace Neo.Cryptography;

public class Ed25519
{
internal const int PublicKeySize = 32;
private const int PrivateKeySize = 32;
internal const int SignatureSize = 64;

/// <summary>
/// Generates a new Ed25519 key pair.
/// </summary>
/// <returns>A byte array containing the private key.</returns>
public static byte[] GenerateKeyPair()
{
var keyPairGenerator = new Ed25519KeyPairGenerator();
keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var keyPair = keyPairGenerator.GenerateKeyPair();
return ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded();
}

/// <summary>
/// Derives the public key from a given private key.
/// </summary>
/// <param name="privateKey">The private key as a byte array.</param>
/// <returns>The corresponding public key as a byte array.</returns>
/// <exception cref="ArgumentException">Thrown when the private key size is invalid.</exception>
public static byte[] GetPublicKey(byte[] privateKey)
{
if (privateKey.Length != PrivateKeySize)
throw new ArgumentException("Invalid private key size", nameof(privateKey));

var privateKeyParams = new Ed25519PrivateKeyParameters(privateKey, 0);
return privateKeyParams.GeneratePublicKey().GetEncoded();
}

/// <summary>
/// Signs a message using the provided private key.
/// Parameters are in the same order as the sample in the Ed25519 specification
/// Ed25519.sign(privkey, pubkey, msg) with pubkey omitted
/// ref. https://datatracker.ietf.org/doc/html/rfc8032.
/// </summary>
/// <param name="privateKey">The private key used for signing.</param>
/// <param name="message">The message to be signed.</param>
/// <returns>The signature as a byte array.</returns>
/// <exception cref="ArgumentException">Thrown when the private key size is invalid.</exception>
public static byte[] Sign(byte[] privateKey, byte[] message)
{
if (privateKey.Length != PrivateKeySize)
throw new ArgumentException("Invalid private key size", nameof(privateKey));

var signer = new Ed25519Signer();
signer.Init(true, new Ed25519PrivateKeyParameters(privateKey, 0));
signer.BlockUpdate(message, 0, message.Length);
return signer.GenerateSignature();
}

/// <summary>
/// Verifies an Ed25519 signature for a given message using the provided public key.
/// Parameters are in the same order as the sample in the Ed25519 specification
/// Ed25519.verify(public, msg, signature)
/// ref. https://datatracker.ietf.org/doc/html/rfc8032.
/// </summary>
/// <param name="publicKey">The 32-byte public key used for verification.</param>
/// <param name="message">The message that was signed.</param>
/// <param name="signature">The 64-byte signature to verify.</param>
/// <returns>True if the signature is valid for the given message and public key; otherwise, false.</returns>
/// <exception cref="ArgumentException">Thrown when the signature or public key size is invalid.</exception>
public static bool Verify(byte[] publicKey, byte[] message, byte[] signature)
{
if (signature.Length != SignatureSize)
throw new ArgumentException("Invalid signature size", nameof(signature));

if (publicKey.Length != PublicKeySize)
throw new ArgumentException("Invalid public key size", nameof(publicKey));

var verifier = new Ed25519Signer();
verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0));
verifier.BlockUpdate(message, 0, message.Length);
return verifier.VerifySignature(signature);
}
}
31 changes: 31 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

using Neo.Cryptography;
using Neo.Cryptography.ECC;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using System;
using System.Collections.Generic;

Expand Down Expand Up @@ -115,5 +117,34 @@ public static bool VerifyWithECDsaV0(byte[] message, byte[] pubkey, byte[] signa
return false;
}
}

/// <summary>
/// Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm.
/// </summary>
/// <param name="message">The signed message.</param>
/// <param name="publicKey">The Ed25519 public key to be used.</param>
/// <param name="signature">The signature to be verified.</param>
/// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
[ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15)]
public static bool VerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature)
{
if (signature.Length != Ed25519.SignatureSize)
return false;

if (publicKey.Length != Ed25519.PublicKeySize)
return false;

try
{
var verifier = new Ed25519Signer();
verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0));
verifier.BlockUpdate(message, 0, message.Length);
return verifier.VerifySignature(signature);
}
catch (Exception)
Copy link
Member

Choose a reason for hiding this comment

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

Well, I was talking only about ArgumentException, e.g. only about input data of invalid length and some other ArgumentException thrown by this part of code. However, catching any exception is a valid solution as far.

@roman-khimov, what do you think about cases when we should return true/false or FAULT VM?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can be both ways, but we need to know all of the exceptions that can happen in this block. Some are likely OK to be converted to false result (invalid signature), some may not (verifier constructor failure?). I'd expect some symmetry to ECDSA verification function. Like what happens if the key is wrong?

{
return false;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Neo/SmartContract/Native/RoleManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@
list.AddRange(nodes);
list.Sort();
engine.SnapshotCache.Add(key, new StorageItem(list));

Jim8y marked this conversation as resolved.
Show resolved Hide resolved
Jim8y marked this conversation as resolved.
Show resolved Hide resolved
if (engine.IsHardforkEnabled(Hardfork.HF_Echidna))
{
var oldNodes = new VM.Types.Array(engine.ReferenceCounter, GetDesignatedByRole(engine.Snapshot, role, index - 1).Select(u => (ByteString)u.EncodePoint(true)));

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test-Everything

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test-Everything

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test-Everything

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test-Everything

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'

Check warning on line 84 in src/Neo/SmartContract/Native/RoleManagement.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'ApplicationEngine.Snapshot' is obsolete: 'This property is deprecated. Use SnapshotCache instead.'
var newNodes = new VM.Types.Array(engine.ReferenceCounter, nodes.Select(u => (ByteString)u.EncodePoint(true)));

engine.SendNotification(Hash, "Designation", new VM.Types.Array(engine.ReferenceCounter, [(int)role, engine.PersistingBlock.Index, oldNodes, newNodes]));
Expand Down
162 changes: 162 additions & 0 deletions tests/Neo.UnitTests/Cryptography/UT_Ed25519.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// UT_Ed25519.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 FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Neo.Cryptography;
using Neo.Extensions;
using Neo.IO;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract;
using Neo.Wallets;
using Neo.Wallets.NEP6;
using System;
using System.Linq;
using System.Text;

namespace Neo.UnitTests.Cryptography
{
[TestClass]
public class UT_Ed25519
{
[TestMethod]
public void TestGenerateKeyPair()
{
byte[] keyPair = Ed25519.GenerateKeyPair();
keyPair.Should().NotBeNull();
keyPair.Length.Should().Be(32);
}

[TestMethod]
public void TestGetPublicKey()
{
byte[] privateKey = Ed25519.GenerateKeyPair();
byte[] publicKey = Ed25519.GetPublicKey(privateKey);
publicKey.Should().NotBeNull();
publicKey.Length.Should().Be(Ed25519.PublicKeySize);
}

[TestMethod]
public void TestSignAndVerify()
{
byte[] privateKey = Ed25519.GenerateKeyPair();
byte[] publicKey = Ed25519.GetPublicKey(privateKey);
byte[] message = Encoding.UTF8.GetBytes("Hello, Neo!");

byte[] signature = Ed25519.Sign(privateKey, message);
signature.Should().NotBeNull();
signature.Length.Should().Be(Ed25519.SignatureSize);

bool isValid = Ed25519.Verify(publicKey, message, signature);
isValid.Should().BeTrue();
}

[TestMethod]
public void TestFailedVerify()
{
byte[] privateKey = Ed25519.GenerateKeyPair();
byte[] publicKey = Ed25519.GetPublicKey(privateKey);
byte[] message = Encoding.UTF8.GetBytes("Hello, Neo!");

byte[] signature = Ed25519.Sign(privateKey, message);

// Tamper with the message
byte[] tamperedMessage = Encoding.UTF8.GetBytes("Hello, Neo?");

bool isValid = Ed25519.Verify(publicKey, tamperedMessage, signature);
isValid.Should().BeFalse();

// Tamper with the signature
byte[] tamperedSignature = new byte[signature.Length];
Array.Copy(signature, tamperedSignature, signature.Length);
tamperedSignature[0] ^= 0x01; // Flip one bit

isValid = Ed25519.Verify(publicKey, message, tamperedSignature);
isValid.Should().BeFalse();

// Use wrong public key
byte[] wrongPrivateKey = Ed25519.GenerateKeyPair();
byte[] wrongPublicKey = Ed25519.GetPublicKey(wrongPrivateKey);

isValid = Ed25519.Verify(wrongPublicKey, message, signature);
isValid.Should().BeFalse();
}

[TestMethod]
public void TestInvalidPrivateKeySize()
{
byte[] invalidPrivateKey = new byte[31]; // Invalid size
Action act = () => Ed25519.GetPublicKey(invalidPrivateKey);
act.Should().Throw<ArgumentException>().WithMessage("Invalid private key size*");
}

[TestMethod]
public void TestInvalidSignatureSize()
{
byte[] message = Encoding.UTF8.GetBytes("Test message");
byte[] invalidSignature = new byte[63]; // Invalid size
byte[] publicKey = new byte[Ed25519.PublicKeySize];
Action act = () => Ed25519.Verify(publicKey, message, invalidSignature);
act.Should().Throw<ArgumentException>().WithMessage("Invalid signature size*");
}

[TestMethod]
public void TestInvalidPublicKeySize()
{
byte[] message = Encoding.UTF8.GetBytes("Test message");
byte[] signature = new byte[Ed25519.SignatureSize];
byte[] invalidPublicKey = new byte[31]; // Invalid size
Action act = () => Ed25519.Verify(invalidPublicKey, message, signature);
act.Should().Throw<ArgumentException>().WithMessage("Invalid public key size*");
}

// Test vectors from RFC 8032 (https://datatracker.ietf.org/doc/html/rfc8032)
// Section 7.1. Test Vectors for Ed25519

[TestMethod]
public void TestVectorCase1()
{
byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes();
byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes();
byte[] message = Array.Empty<byte>();
byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" +
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes();

Ed25519.GetPublicKey(privateKey).Should().Equal(publicKey);
Ed25519.Sign(privateKey, message).Should().Equal(signature);
}

[TestMethod]
public void TestVectorCase2()
{
byte[] privateKey = "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb".HexToBytes();
byte[] publicKey = "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c".HexToBytes();
byte[] message = Encoding.UTF8.GetBytes("r");
byte[] signature = ("92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da" +
"085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00").HexToBytes();

Ed25519.GetPublicKey(privateKey).Should().Equal(publicKey);
Ed25519.Sign(privateKey, message).Should().Equal(signature);
}

[TestMethod]
public void TestVectorCase3()
{
byte[] privateKey = "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7".HexToBytes();
byte[] publicKey = "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025".HexToBytes();
byte[] signature = ("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac" +
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a").HexToBytes();
byte[] message = "af82".HexToBytes();
Ed25519.GetPublicKey(privateKey).Should().Equal(publicKey);
Ed25519.Sign(privateKey, message).Should().Equal(signature);
}
}
}
55 changes: 55 additions & 0 deletions tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Neo.UnitTests.SmartContract.Native
{
Expand Down Expand Up @@ -910,5 +911,59 @@ private bool CallVerifyWithECDsa(byte[] message, ECPoint pub, byte[] signature,
return engine.ResultStack.Pop().GetBoolean();
}
}

[TestMethod]
public void TestVerifyWithEd25519()
{
byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes();
byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes();
byte[] message = Array.Empty<byte>();
byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" +
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes();

// Verify using Ed25519 directly
Ed25519.Verify(publicKey, message, signature).Should().BeTrue();

// Verify using CryptoLib.VerifyWithEd25519
CallVerifyWithEd25519(message, publicKey, signature).Should().BeTrue();

// Test with a different message
byte[] differentMessage = Encoding.UTF8.GetBytes("Different message");
CallVerifyWithEd25519(differentMessage, publicKey, signature).Should().BeFalse();

// Test with an invalid signature
byte[] invalidSignature = new byte[signature.Length];
Array.Copy(signature, invalidSignature, signature.Length);
invalidSignature[0] ^= 0x01; // Flip one bit
CallVerifyWithEd25519(message, publicKey, invalidSignature).Should().BeFalse();

// Test with an invalid public key
byte[] invalidPublicKey = new byte[publicKey.Length];
Array.Copy(publicKey, invalidPublicKey, publicKey.Length);
invalidPublicKey[0] ^= 0x01; // Flip one bit
CallVerifyWithEd25519(message, invalidPublicKey, signature).Should().BeFalse();
}

private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature)
{
var snapshot = TestBlockchain.GetTestSnapshotCache();
using (ScriptBuilder script = new())
{
script.EmitPush(signature);
script.EmitPush(publicKey);
script.EmitPush(message);
script.EmitPush(3);
script.Emit(OpCode.PACK);
script.EmitPush(CallFlags.All);
script.EmitPush("verifyWithEd25519");
script.EmitPush(NativeContract.CryptoLib.Hash);
script.EmitSysCall(ApplicationEngine.System_Contract_Call);

using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings);
engine.LoadScript(script.ToArray());
Assert.AreEqual(VMState.HALT, engine.Execute());
return engine.ResultStack.Pop().GetBoolean();
}
}
}
}
Loading
Loading