Skip to content

Commit

Permalink
[Core Add] Add support to Ed25519 (#3507)
Browse files Browse the repository at this point in the history
* fix unnecessary change

* Clean using

---------

Co-authored-by: Fernando Diaz Toledano <[email protected]>
  • Loading branch information
Jim8y and shargon authored Jan 9, 2025
1 parent 93fc37b commit b1a3ea3
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 2 deletions.
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;

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)
{
return false;
}
}
}
}
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

0 comments on commit b1a3ea3

Please sign in to comment.