From 661db9302a6b113760a1f342954e6db968507838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Mon, 6 Jul 2026 20:54:05 +0200 Subject: [PATCH 01/17] Private Ed25519 key derivation based on firmware's XEdDSA::priv_curve_to_ed_keys --- Meshtastic/Crypto/XEdDSASigning.cs | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index a94ec17..c747a08 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -41,32 +41,9 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 if (x25519PrivateKey.Length != 32) throw new ArgumentException("X25519 private key must be 32 bytes", nameof(x25519PrivateKey)); - // For simplicity, use the X25519 key as seed for Ed25519 key generation - // In a full XEdDSA implementation, this would use proper key derivation - var hashedSeed = SHA256.HashData(x25519PrivateKey); - - var keyPairGen = new Ed25519KeyPairGenerator(); - var secureRandom = new SecureRandom(); - secureRandom.SetSeed(hashedSeed); - keyPairGen.Init(new KeyGenerationParameters(secureRandom, 256)); - - var keyPair = keyPairGen.GenerateKeyPair(); - // Proper XEdDSA key derivation with domain separation - // Ed25519 seed = SHA-512("XEdDSA" || x25519PrivateKey)[0..31] - byte[] domain = Encoding.ASCII.GetBytes("XEdDSA"); - byte[] input = new byte[domain.Length + x25519PrivateKey.Length]; - Buffer.BlockCopy(domain, 0, input, 0, domain.Length); - Buffer.BlockCopy(x25519PrivateKey, 0, input, domain.Length, x25519PrivateKey.Length); - byte[] hash = SHA512.HashData(input); - byte[] ed25519Seed = new byte[32]; - Array.Copy(hash, 0, ed25519Seed, 0, 32); - - // Create Ed25519 private key from seed - var edPrivateKeyParam = new Ed25519PrivateKeyParameters(ed25519Seed, 0); - var edPublicKeyParam = edPrivateKeyParam.GeneratePublicKey(); - var privateKey = edPrivateKeyParam.GetEncoded(); - var publicKey = edPublicKeyParam.GetEncoded(); - return (privateKey, publicKey); + var x25519PublicKey = PKIEncryption.GetPublicKeyFromPrivateKey(x25519PrivateKey); + var ed25519PublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey); + return (x25519PrivateKey, ed25519PublicKey); } /// From 3c4b75315dd54d5012695ffacd80cb08cb91114c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Mon, 6 Jul 2026 20:55:14 +0200 Subject: [PATCH 02/17] Removed double hashing to stay in line with firmware's implementation --- Meshtastic/Crypto/XEdDSASigning.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index c747a08..b7fd6c4 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -113,20 +113,17 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) /// Ed25519 private key /// Use SHA-256 instead of SHA-512 for hashing before signing /// 64-byte signature - public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKey, bool useShortHash = true) + public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKey) { if (message == null) throw new ArgumentNullException(nameof(message)); if (edPrivateKey.Length != 32) throw new ArgumentException("Ed25519 private key must be 32 bytes", nameof(edPrivateKey)); - // Hash the message using the selected algorithm - var messageHash = useShortHash ? SHA256.HashData(message) : SHA512.HashData(message); - // Create Ed25519 signer var signer = new Ed25519Signer(); var privateKeyParams = new Ed25519PrivateKeyParameters(edPrivateKey, 0); signer.Init(true, privateKeyParams); - signer.BlockUpdate(messageHash, 0, messageHash.Length); + signer.BlockUpdate(message, 0, message.Length); return signer.GenerateSignature(); } @@ -139,7 +136,7 @@ public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKe /// Ed25519 public key of the signer /// Use SHA-256 instead of SHA-512 for hashing /// True if signature is valid - public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey, bool useShortHash = true) + public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey) { if (message == null) throw new ArgumentNullException(nameof(message)); if (signature == null || signature.Length != 64) throw new ArgumentException("Signature must be 64 bytes", nameof(signature)); @@ -147,15 +144,12 @@ public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey, try { - // Hash the message using the selected algorithm - var messageHash = useShortHash ? SHA256.HashData(message) : SHA512.HashData(message); - // Create Ed25519 verifier var verifier = new Ed25519Signer(); var publicKeyParams = new Ed25519PublicKeyParameters(edPublicKey, 0); verifier.Init(false, publicKeyParams); - verifier.BlockUpdate(messageHash, 0, messageHash.Length); + verifier.BlockUpdate(message, 0, message.Length); return verifier.VerifySignature(signature); } @@ -173,13 +167,13 @@ public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey, /// X25519 public key of the signer /// Use SHA-256 instead of SHA-512 for verification /// True if signature is valid - public static bool VerifyWithX25519Key(byte[] message, byte[] signature, byte[] x25519PublicKey, bool useShortHash = true) + public static bool VerifyWithX25519Key(byte[] message, byte[] signature, byte[] x25519PublicKey) { try { // Convert X25519 public key to Ed25519 public key var edPublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey); - return Verify(message, signature, edPublicKey, useShortHash); + return Verify(message, signature, edPublicKey); } catch { From 77b44b27349e5eeef75961e4bae8e163feba2639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Mon, 6 Jul 2026 20:57:01 +0200 Subject: [PATCH 03/17] Removed unused code --- Meshtastic/Crypto/XEdDSASigning.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index b7fd6c4..3ada1c7 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -56,18 +56,6 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) if (x25519PublicKey.Length != 32) throw new ArgumentException("X25519 public key must be 32 bytes", nameof(x25519PublicKey)); - // Simplified conversion - in practice would use proper birational map - // For demo purposes, derive deterministic Ed25519 key from X25519 key - var hashedKey = SHA256.HashData(x25519PublicKey); - - var keyPairGen = new Ed25519KeyPairGenerator(); - var secureRandom = new SecureRandom(); - secureRandom.SetSeed(hashedKey); - keyPairGen.Init(new KeyGenerationParameters(secureRandom, 256)); - - var keyPair = keyPairGen.GenerateKeyPair(); - var edPublicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded(); - // Clear the sign bit (bit 7 of the last byte) as per Ed25519 specification // Implements the birational map from Montgomery (X25519) u to Edwards (Ed25519) y: // y = (u - 1) / (u + 1) mod p From 9d39e89f8df3f4d5908bf5770e32df53e1159785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Mon, 6 Jul 2026 21:00:46 +0200 Subject: [PATCH 04/17] Removed useShortHash references --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 28 ++++--------------- .../NodeInfoMessageFactory.cs | 22 ++++++--------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs index 9f51479..4bd7f2b 100644 --- a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs +++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs @@ -58,7 +58,7 @@ public void Sign_Should_ProduceValidSignature() var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey); // Act - var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash: true); + var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey); // Assert Assert.That(signature, Is.Not.Null); @@ -72,10 +72,10 @@ public void Verify_Should_ReturnTrue_ForValidSignature() // Arrange var message = Encoding.UTF8.GetBytes("Test message for verification"); var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey); - var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash: true); + var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey); // Act - var isValid = XEdDSASigning.Verify(message, signature, edPublicKey, useShortHash: true); + var isValid = XEdDSASigning.Verify(message, signature, edPublicKey); // Assert Assert.That(isValid, Is.True); @@ -90,7 +90,7 @@ public void Verify_Should_ReturnFalse_ForInvalidSignature() var (_, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey); // Act - var isValid = XEdDSASigning.Verify(message, invalidSignature, edPublicKey, useShortHash: true); + var isValid = XEdDSASigning.Verify(message, invalidSignature, edPublicKey); // Assert Assert.That(isValid, Is.False); @@ -103,31 +103,15 @@ public void Verify_Should_ReturnFalse_ForTamperedMessage() var originalMessage = Encoding.UTF8.GetBytes("Original message"); var tamperedMessage = Encoding.UTF8.GetBytes("Tampered message"); var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey); - var signature = XEdDSASigning.Sign(originalMessage, edPrivateKey, edPublicKey, useShortHash: true); + var signature = XEdDSASigning.Sign(originalMessage, edPrivateKey, edPublicKey); // Act - var isValid = XEdDSASigning.Verify(tamperedMessage, signature, edPublicKey, useShortHash: true); + var isValid = XEdDSASigning.Verify(tamperedMessage, signature, edPublicKey); // Assert Assert.That(isValid, Is.False); } - [TestCase(true)] - [TestCase(false)] - public void SignAndVerify_Should_Work_WithBothHashTypes(bool useShortHash) - { - // Arrange - var message = Encoding.UTF8.GetBytes("Test message for both hash types"); - var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey); - - // Act - var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash); - var isValid = XEdDSASigning.Verify(message, signature, edPublicKey, useShortHash); - - // Assert - Assert.That(isValid, Is.True); - } - [Test] public void Sign_Should_ThrowException_ForNullMessage() { diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index e4cad69..5000c3f 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -17,13 +17,11 @@ public static class NodeInfoMessageFactory /// Device state containing configuration /// User information to broadcast /// Whether to sign the packet with XEdDSA - /// Use SHA-256 instead of SHA-512 for signatures /// MeshPacket containing the NodeInfo public static MeshPacket CreateNodeInfoMessage( DeviceStateContainer deviceStateContainer, User user, - bool signPacket = false, - bool useShortHash = true) + bool signPacket = false) { if (deviceStateContainer == null) throw new ArgumentNullException(nameof(deviceStateContainer)); @@ -55,7 +53,7 @@ public static MeshPacket CreateNodeInfoMessage( { try { - AddXEdDSASignature(meshPacket, deviceStateContainer, useShortHash); + AddXEdDSASignature(meshPacket, deviceStateContainer); } catch (Exception) { @@ -106,12 +104,10 @@ public static User CreateTestUser( /// /// Received mesh packet /// Public key of the sender - /// Use SHA-256 instead of SHA-512 /// True if signature is valid public static bool VerifyNodeInfoSignature( MeshPacket meshPacket, - byte[] senderPublicKey, - bool useShortHash = true) + byte[] senderPublicKey) { if (meshPacket?.Decoded?.Payload == null) return false; @@ -127,7 +123,7 @@ public static bool VerifyNodeInfoSignature( // For now, we'll demonstrate the verification process var mockSignature = new byte[64]; // This would come from the packet - return XEdDSASigning.Verify(payload, mockSignature, senderPublicKey, useShortHash); + return XEdDSASigning.Verify(payload, mockSignature, senderPublicKey); } catch { @@ -142,7 +138,7 @@ private static bool HasSigningCapability(DeviceStateContainer deviceStateContain return deviceStateContainer.LocalConfig?.Security?.PublicKey != null; } - private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContainer deviceStateContainer, bool useShortHash) + private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContainer deviceStateContainer) { if (meshPacket.Decoded?.Payload == null) return; @@ -157,7 +153,7 @@ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContain // Sign the payload var payload = meshPacket.Decoded.Payload.ToByteArray(); - var signature = XEdDSASigning.Sign(payload, edPrivateKey, edPublicKey, useShortHash); + var signature = XEdDSASigning.Sign(payload, edPrivateKey, edPublicKey); // Note: In the actual implementation, we would add the signature to the packet // For now, we'll store it in a custom field or metadata @@ -200,14 +196,12 @@ public static NodeInfoPayloadAnalysis AnalyzePayloadSizes(User user) var signedSha256Packet = CreateNodeInfoMessage( deviceStateContainer: testDeviceState, user: user, - signPacket: true, - useShortHash: true); + signPacket: true); var signedSha512Packet = CreateNodeInfoMessage( deviceStateContainer: testDeviceState, user: user, - signPacket: true, - useShortHash: false); + signPacket: true); // Since signing currently fails silently due to no private key, // we'll simulate the signature overhead by creating packets with mock signatures From bdd3adea45911220ee2ddbb8df6945a8fb61303e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 07:49:17 +0200 Subject: [PATCH 05/17] Added test cases for signatures produced by 2.8 firmware --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs index 4bd7f2b..0f08f07 100644 --- a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs +++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs @@ -112,6 +112,38 @@ public void Verify_Should_ReturnFalse_ForTamperedMessage() Assert.That(isValid, Is.False); } + [Test] + public void Verify_RealLife_ValidSignature() + { + var rawCapturedPacket = Convert.FromBase64String("DbVdZX4V/////xgVIkwIARIEVGVzdEgBUkDdbuxwz2lvDyBKpCW1ojj+pMPfnRfWiUsDwf1cwisx+82L7fA5/g5OW5LrpWfU4z73AHqysNLKBUOt3TfhBEQONXE20CI9ieBNakgHWGR4B5gBtQE="); + var senderPublicKey = Convert.FromBase64String("t0hKwMywRb2nKFOvVXcjFAGPWgCSta4ZwEkgPkgJWwM="); + var meshPacket = MeshPacket.Parser.ParseFrom(rawCapturedPacket); + var isValid = XEdDSASigning.VerifyPacketSignature(senderPublicKey, meshPacket); + Assert.That(isValid, Is.True); + } + + [Test] + public void Verify_RealLife_InvalidPubkey() + { + var rawCapturedPacket = Convert.FromBase64String("DbVdZX4V/////xgVIkwIARIEVGVzdEgBUkDdbuxwz2lvDyBKpCW1ojj+pMPfnRfWiUsDwf1cwisx+82L7fA5/g5OW5LrpWfU4z73AHqysNLKBUOt3TfhBEQONXE20CI9ieBNakgHWGR4B5gBtQE="); + var senderPublicKey = Convert.FromBase64String("t0hKwMywRb2nKFOvVXcjFAGPWgCSta4ZwEkgPkgJWwM="); + senderPublicKey[0]++; + var meshPacket = MeshPacket.Parser.ParseFrom(rawCapturedPacket); + var isValid = XEdDSASigning.VerifyPacketSignature(senderPublicKey, meshPacket); + Assert.That(isValid, Is.False); + } + + [Test] + public void Verify_RealLife_TamperedMessage() + { + var rawCapturedPacket = Convert.FromBase64String("DbVdZX4V/////xgVIkwIARIEVGVzdEgBUkDdbuxwz2lvDyBKpCW1ojj+pMPfnRfWiUsDwf1cwisx+82L7fA5/g5OW5LrpWfU4z73AHqysNLKBUOt3TfhBEQONXE20CI9ieBNakgHWGR4B5gBtQE="); + var senderPublicKey = Convert.FromBase64String("t0hKwMywRb2nKFOvVXcjFAGPWgCSta4ZwEkgPkgJWwM="); + var meshPacket = MeshPacket.Parser.ParseFrom(rawCapturedPacket); + meshPacket.Decoded.Payload = Google.Protobuf.ByteString.CopyFromUtf8("Tampered"); + var isValid = XEdDSASigning.VerifyPacketSignature(senderPublicKey, meshPacket); + Assert.That(isValid, Is.False); + } + [Test] public void Sign_Should_ThrowException_ForNullMessage() { From a69d61b3d51072464ab29f5a4a18d0737cd9f99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 07:54:23 +0200 Subject: [PATCH 06/17] Helper for verifying MeshPacket signature --- Meshtastic/Crypto/XEdDSASigning.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 3ada1c7..82469e6 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -6,6 +6,7 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Math; using System.Text; +using Meshtastic.Protobufs; namespace Meshtastic.Crypto; @@ -94,6 +95,24 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) return edPublicKeyResult; } + private static byte[] BuildSigningBuffer(MeshPacket meshPacket) + { + return [ + ..BitConverter.GetBytes(meshPacket.From), + ..BitConverter.GetBytes(meshPacket.Id), + ..BitConverter.GetBytes((uint)meshPacket.Decoded.Portnum), + ..meshPacket.Decoded.Payload.ToByteArray() + ]; + } + + public static bool VerifyPacketSignature(byte[] senderPublicKey, MeshPacket meshPacket) + { + var message = BuildSigningBuffer(meshPacket); + var signature = meshPacket.Decoded.XeddsaSignature.ToByteArray(); + var edPublicKey = ConvertX25519PublicKeyToEd25519(senderPublicKey); + return Verify(message, signature, edPublicKey); + } + /// /// Sign a message using Ed25519 /// From eea93b5434daf2bd5bbcf0533a5953262f904a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 07:56:50 +0200 Subject: [PATCH 07/17] Helper for adding MeshPacket signature --- Meshtastic/Crypto/XEdDSASigning.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 82469e6..bf55af8 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -113,6 +113,14 @@ public static bool VerifyPacketSignature(byte[] senderPublicKey, MeshPacket mesh return Verify(message, signature, edPublicKey); } + public static void AddPacketSignature(byte[] senderPrivateKey, MeshPacket meshPacket) + { + var message = BuildSigningBuffer(meshPacket); + var (edPrivateKey, edPublicKey) = GenerateEdDSAKeysFromX25519(senderPrivateKey); + var signature = Sign(message, edPrivateKey, edPublicKey); + meshPacket.Decoded.XeddsaSignature = Google.Protobuf.ByteString.CopyFrom(signature); + } + /// /// Sign a message using Ed25519 /// From b74e3f9e9e56f405414418a4cbdd36880f194693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 07:58:32 +0200 Subject: [PATCH 08/17] Force positive sign on Ed25519 key derived from X25519 --- Meshtastic/Crypto/XEdDSASigning.cs | 32 ++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index bf55af8..cf3c4c2 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -42,9 +42,33 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 if (x25519PrivateKey.Length != 32) throw new ArgumentException("X25519 private key must be 32 bytes", nameof(x25519PrivateKey)); + var ed25519PrivateKey = new byte[32]; + Array.Copy(x25519PrivateKey, ed25519PrivateKey, ed25519PrivateKey.Length); + // Clamp X25519 + ed25519PrivateKey[0] &= 0xF8; + ed25519PrivateKey[31] &= 0x7F; + ed25519PrivateKey[31] |= 0x40; + var x25519PublicKey = PKIEncryption.GetPublicKeyFromPrivateKey(x25519PrivateKey); - var ed25519PublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey); - return (x25519PrivateKey, ed25519PublicKey); + var ed25519PublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); + + // If resulting public key is positive, return as-is + if ((ed25519PublicKey[31] & 0x80) == 0) return (x25519PrivateKey, ed25519PublicKey); + + // Ed25519 Group Order + var L = new BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989"); + + // Negate private key + var privateKeyScalar = new BigInteger(ed25519PrivateKey.Reverse().ToArray()); + var negatedPrivateKeyScalar = L.Subtract(privateKeyScalar); + var negatedPrivateKeyBytes = negatedPrivateKeyScalar.ToByteArrayUnsigned(); + byte[] negatedPrivateKey = new byte[32]; + Array.Copy(negatedPrivateKeyBytes, 0, negatedPrivateKey, 0, negatedPrivateKeyBytes.Length); + + // Recompute public key from negated privated key + var negatedPublicKey = ConvertX25519PublicKeyToEd25519(negatedPrivateKey); + + return (negatedPrivateKey, negatedPublicKey); } /// @@ -52,7 +76,7 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 /// /// 32-byte X25519 public key /// 32-byte Ed25519 public key - public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) + public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey, bool forcePositive = false) { if (x25519PublicKey.Length != 32) throw new ArgumentException("X25519 public key must be 32 bytes", nameof(x25519PublicKey)); @@ -91,7 +115,7 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) // If yBytes is shorter than 32 bytes, the rest is already zero // Set the sign bit to 0 (positive x) - edPublicKeyResult[31] &= 0x7F; + if (forcePositive) edPublicKeyResult[31] &= 0x7F; return edPublicKeyResult; } From 869330be36cd417e7097170f177d19c22f1031f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 08:04:27 +0200 Subject: [PATCH 09/17] Added docs --- Meshtastic/Crypto/XEdDSASigning.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index cf3c4c2..25a338c 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -129,6 +129,12 @@ private static byte[] BuildSigningBuffer(MeshPacket meshPacket) ]; } + /// + /// Verify MeshPacket signature using provided node's X25519 public key. + /// + /// Public key of sender node. + /// Packet to verify. + /// True is packet signature is valid public static bool VerifyPacketSignature(byte[] senderPublicKey, MeshPacket meshPacket) { var message = BuildSigningBuffer(meshPacket); @@ -137,6 +143,11 @@ public static bool VerifyPacketSignature(byte[] senderPublicKey, MeshPacket mesh return Verify(message, signature, edPublicKey); } + /// + /// Adds a signature to provided MeshPacket. + /// + /// Private X25519 key of packet sender. + /// Packet to sign. public static void AddPacketSignature(byte[] senderPrivateKey, MeshPacket meshPacket) { var message = BuildSigningBuffer(meshPacket); From ded4b1b28c219cb973f824aaca742efff3d7bc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 09:45:36 +0200 Subject: [PATCH 10/17] Test case for Ed25519 public key derived from private key matched the one derived from X25519 public key --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs index 0f08f07..cb5e807 100644 --- a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs +++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs @@ -50,6 +50,15 @@ public void ConvertX25519PublicKeyToEd25519_Should_ProduceValidEdPublicKey() Assert.That(edPublicKey[31] & 0x80, Is.EqualTo(0)); // Sign bit should be 0 } + [Test] + public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519() + { + var (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); + var (edPrivateKey, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); + var edPublicKeyFromPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey); + Assert.That(edPublicKeyFromPrivate, Is.EqualTo(edPublicKeyFromPublic)); + } + [Test] public void Sign_Should_ProduceValidSignature() { From fbfbf2861f83d84577cf12b16bbf09f7352148e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 13:15:41 +0200 Subject: [PATCH 11/17] Test both positive and negative (scalar sign-flipping) GenerateEdDSAKeysFromX25519 paths --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs index cb5e807..c84a64e 100644 --- a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs +++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs @@ -51,11 +51,40 @@ public void ConvertX25519PublicKeyToEd25519_Should_ProduceValidEdPublicKey() } [Test] - public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519() + public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519_Positive() { - var (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); - var (edPrivateKey, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); + byte[] x25519PrivateKey; + byte[] x25519PublicKey; + + // Generate X25519 keypairs until one producing positive Ed25519 is found + while (true) + { + (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); + var edPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); + if ((edPublic[31] & 0x80) == 0) break; + } + + var edPublicKeyFromPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey); + var (_, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); + Assert.That(edPublicKeyFromPrivate, Is.EqualTo(edPublicKeyFromPublic)); + } + + [Test] + public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519_Negative() + { + byte[] x25519PrivateKey; + byte[] x25519PublicKey; + + // Generate X25519 keypairs until one producing negative Ed25519 is found + while (true) + { + (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); + var edPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); + if ((edPublic[31] & 0x80) != 0) break; + } + var edPublicKeyFromPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey); + var (_, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); Assert.That(edPublicKeyFromPrivate, Is.EqualTo(edPublicKeyFromPublic)); } From 10286f562bf0b34e6127a21775703cfaa860408e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 13:18:05 +0200 Subject: [PATCH 12/17] Proper derivation of public Ed25519 key from private key --- Meshtastic/Crypto/XEdDSASigning.cs | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 25a338c..a566bdc 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -32,6 +32,28 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateSigningKeyPair() return (privateKey, publicKey); } + private static byte[] GeneratePublicKeyFromPrivateKey(byte[] ed25519PrivateKey) + { + /* + * Normally the public key should be obtained by calling: + * new Ed25519PrivateKeyParameters(ed25519PrivateKey).GeneratePublicKey().GetEncoded() + * but in firmware's function XEdDSA::priv_curve_to_ed_keys private key is used as-is + * as a scalar, while BouncyCastle's GeneratePublicKey first passes private key material + * through SHA512 to obtain 64-byte seed, of which 2nd half is used as a scalar and multiplied + * by curve's base point to obtain public key. + * + * Reimplementing field multiplication, as provided by ScalarMultBaseEncoded, in this project + * seems pointless - hence use of reflection to gain access to this primitive to generate + * a public key directly from provided material. + */ + + var ed25519PublicKey = new byte[32]; + typeof(Org.BouncyCastle.Math.EC.Rfc8032.Ed25519) + .GetMethod("ScalarMultBaseEncoded", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, [typeof(byte[]), typeof(byte[]), typeof(int)])! + .Invoke(null, [ed25519PrivateKey, ed25519PublicKey, 0]); + return ed25519PublicKey; + } + /// /// Generate Ed25519 keys from an X25519 private key (simplified for demo) /// @@ -44,29 +66,33 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 var ed25519PrivateKey = new byte[32]; Array.Copy(x25519PrivateKey, ed25519PrivateKey, ed25519PrivateKey.Length); + // Clamp X25519 ed25519PrivateKey[0] &= 0xF8; ed25519PrivateKey[31] &= 0x7F; ed25519PrivateKey[31] |= 0x40; var x25519PublicKey = PKIEncryption.GetPublicKeyFromPrivateKey(x25519PrivateKey); - var ed25519PublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); + var ed25519PublicKey = GeneratePublicKeyFromPrivateKey(ed25519PrivateKey); // If resulting public key is positive, return as-is - if ((ed25519PublicKey[31] & 0x80) == 0) return (x25519PrivateKey, ed25519PublicKey); + if ((ed25519PublicKey[31] & 0x80) == 0) return (ed25519PrivateKey, ed25519PublicKey); // Ed25519 Group Order var L = new BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989"); // Negate private key - var privateKeyScalar = new BigInteger(ed25519PrivateKey.Reverse().ToArray()); + var privateKeyScalar = new BigInteger(1, ed25519PrivateKey.Reverse().ToArray()); var negatedPrivateKeyScalar = L.Subtract(privateKeyScalar); var negatedPrivateKeyBytes = negatedPrivateKeyScalar.ToByteArrayUnsigned(); byte[] negatedPrivateKey = new byte[32]; - Array.Copy(negatedPrivateKeyBytes, 0, negatedPrivateKey, 0, negatedPrivateKeyBytes.Length); + for (int i = 0; i < negatedPrivateKeyBytes.Length && i < 32; i++) + { + negatedPrivateKey[i] = negatedPrivateKeyBytes[negatedPrivateKeyBytes.Length - 1 - i]; + } // Recompute public key from negated privated key - var negatedPublicKey = ConvertX25519PublicKeyToEd25519(negatedPrivateKey); + var negatedPublicKey = GeneratePublicKeyFromPrivateKey(negatedPrivateKey); return (negatedPrivateKey, negatedPublicKey); } From 403d184b18e2237b2fb6c6768ac2cce6839f0113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 15:33:33 +0200 Subject: [PATCH 13/17] Reduce scalar before negating --- Meshtastic/Crypto/XEdDSASigning.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index a566bdc..15cf99b 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -83,7 +83,7 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 // Negate private key var privateKeyScalar = new BigInteger(1, ed25519PrivateKey.Reverse().ToArray()); - var negatedPrivateKeyScalar = L.Subtract(privateKeyScalar); + var negatedPrivateKeyScalar = L.Subtract(privateKeyScalar.Mod(L)); var negatedPrivateKeyBytes = negatedPrivateKeyScalar.ToByteArrayUnsigned(); byte[] negatedPrivateKey = new byte[32]; for (int i = 0; i < negatedPrivateKeyBytes.Length && i < 32; i++) From f3f475d17967701b38d5b510571237b74803445a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 15:39:47 +0200 Subject: [PATCH 14/17] Deterministic test cases for positive/negative scalar sign during public key generation --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 26 +++----------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs index c84a64e..aef385e 100644 --- a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs +++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs @@ -53,17 +53,8 @@ public void ConvertX25519PublicKeyToEd25519_Should_ProduceValidEdPublicKey() [Test] public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519_Positive() { - byte[] x25519PrivateKey; - byte[] x25519PublicKey; - - // Generate X25519 keypairs until one producing positive Ed25519 is found - while (true) - { - (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); - var edPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); - if ((edPublic[31] & 0x80) == 0) break; - } - + var x25519PrivateKey = Convert.FromBase64String("+ATTWKM68dlArUzWrXWlQm45Gi3ZYrOdDt2z5VT5+2o="); + var x25519PublicKey = PKIEncryption.GetPublicKeyFromPrivateKey(x25519PrivateKey); var edPublicKeyFromPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey); var (_, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); Assert.That(edPublicKeyFromPrivate, Is.EqualTo(edPublicKeyFromPublic)); @@ -72,17 +63,8 @@ public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX [Test] public void ConvertX25519PublicKeyToEd25519_Should_EqualToGenerateEdDSAKeysFromX25519_Negative() { - byte[] x25519PrivateKey; - byte[] x25519PublicKey; - - // Generate X25519 keypairs until one producing negative Ed25519 is found - while (true) - { - (x25519PrivateKey, x25519PublicKey) = PKIEncryption.GenerateKeyPair(); - var edPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey, forcePositive: false); - if ((edPublic[31] & 0x80) != 0) break; - } - + var x25519PrivateKey = Convert.FromBase64String("wJNheemu5n2oPgpu0BpEdomsPlChBSM8gAO7RRWkT2w="); + var x25519PublicKey = PKIEncryption.GetPublicKeyFromPrivateKey(x25519PrivateKey); var edPublicKeyFromPublic = XEdDSASigning.ConvertX25519PublicKeyToEd25519(x25519PublicKey); var (_, edPublicKeyFromPrivate) = XEdDSASigning.GenerateEdDSAKeysFromX25519(x25519PrivateKey); Assert.That(edPublicKeyFromPrivate, Is.EqualTo(edPublicKeyFromPublic)); From 1cc60e13dd0db5c606dc5ef76d511eb0eb23b495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Wed, 8 Jul 2026 16:05:00 +0200 Subject: [PATCH 15/17] Use Ed25519 Sign/Verify primitives directly --- Meshtastic/Crypto/XEdDSASigning.cs | 38 +++++++++--------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 15cf99b..cd76d89 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -7,6 +7,7 @@ using Org.BouncyCastle.Math; using System.Text; using Meshtastic.Protobufs; +using Org.BouncyCastle.Math.EC.Rfc8032; namespace Meshtastic.Crypto; @@ -187,21 +188,20 @@ public static void AddPacketSignature(byte[] senderPrivateKey, MeshPacket meshPa /// /// Message to sign /// Ed25519 private key - /// Use SHA-256 instead of SHA-512 for hashing before signing + /// Ed25519 public key /// 64-byte signature public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKey) { if (message == null) throw new ArgumentNullException(nameof(message)); if (edPrivateKey.Length != 32) throw new ArgumentException("Ed25519 private key must be 32 bytes", nameof(edPrivateKey)); - - // Create Ed25519 signer - var signer = new Ed25519Signer(); - var privateKeyParams = new Ed25519PrivateKeyParameters(edPrivateKey, 0); - - signer.Init(true, privateKeyParams); - signer.BlockUpdate(message, 0, message.Length); - - return signer.GenerateSignature(); + var signature = new byte[64]; + /* + * Ed25519Signer computes public key from private key differently than firmware (see comment for GeneratePublicKeyFromPrivateKey) + * and causes signature verification by actual radios to fail, so instead use Ed25519 primitive directly, explicitly passing + * public key computed derived as in XEdDSA::priv_curve_to_ed_keys. + */ + Ed25519.Sign(edPrivateKey, 0, edPublicKey, 0, null, message, 0, message.Length, signature, 0); + return signature; } /// @@ -210,29 +210,13 @@ public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKe /// Original message /// 64-byte signature /// Ed25519 public key of the signer - /// Use SHA-256 instead of SHA-512 for hashing /// True if signature is valid public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey) { if (message == null) throw new ArgumentNullException(nameof(message)); if (signature == null || signature.Length != 64) throw new ArgumentException("Signature must be 64 bytes", nameof(signature)); if (edPublicKey == null || edPublicKey.Length != 32) throw new ArgumentException("Ed25519 public key must be 32 bytes", nameof(edPublicKey)); - - try - { - // Create Ed25519 verifier - var verifier = new Ed25519Signer(); - var publicKeyParams = new Ed25519PublicKeyParameters(edPublicKey, 0); - - verifier.Init(false, publicKeyParams); - verifier.BlockUpdate(message, 0, message.Length); - - return verifier.VerifySignature(signature); - } - catch - { - return false; - } + return Ed25519.Verify(signature, 0, edPublicKey, 0, message, 0, message.Length); } /// From 28cbbcb92af53fef47f2893fb8bd71e1ca274938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Thu, 9 Jul 2026 10:47:36 +0200 Subject: [PATCH 16/17] Sign method, as expected by the firmware --- Meshtastic/Crypto/XEdDSASigning.cs | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index cd76d89..5acce98 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -8,6 +8,7 @@ using System.Text; using Meshtastic.Protobufs; using Org.BouncyCastle.Math.EC.Rfc8032; +using Org.BouncyCastle.Crypto.Digests; namespace Meshtastic.Crypto; @@ -199,8 +200,50 @@ public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKe * Ed25519Signer computes public key from private key differently than firmware (see comment for GeneratePublicKeyFromPrivateKey) * and causes signature verification by actual radios to fail, so instead use Ed25519 primitive directly, explicitly passing * public key computed derived as in XEdDSA::priv_curve_to_ed_keys. + * + * Also, Ed25519.Sign passes sk (private key) through SHA-512 before feeding it to actual ImplSign, which for some reason + * later fails to verify both by firmware and by Ed25519.Verify. + * + * The following code is inlined from BouncyCastle's Ed25519 class, method: + * private static void ImplSign(byte[] sk, int skOff, byte[] pk, int pkOff, byte[] ctx, byte phflag, byte[] m, int mOff, int mLen, byte[] sig, int sigOff) */ - Ed25519.Sign(edPrivateKey, 0, edPublicKey, 0, null, message, 0, message.Length, signature, 0); + + var digest = new Sha512Digest(); + var h = new byte[64]; + + digest.BlockUpdate(edPrivateKey, 0, 32); + digest.DoFinal(h, 0); + + typeof(Ed25519) + .GetMethod("ImplSign", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, + [ + typeof(IDigest), + typeof(byte[]), + typeof(byte[]), + typeof(byte[]), + typeof(int), + typeof(byte[]), + typeof(byte), + typeof(byte[]), + typeof(int), + typeof(int), + typeof(byte[]), + typeof(int) + ])! + .Invoke(null, [ + /* IDigest d */ digest, + /* byte[] h */ h, + /* byte[] s */ edPrivateKey /* original argument: s, scalar computed from h */, + /* byte[] pk */ edPublicKey, + /* int pkOff */ 0, + /* byte[] ctx */ null, + /* byte phflag */ (byte)0, + /* byte[] */ message, + /* int mOff*/ 0, + /* int mLen */ message.Length, + /* byte[] sig */ signature, + /* int sigOff */ 0]); + return signature; } From 812b0f6a5dbfd39397f2d942a815acf49a305c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Thu, 9 Jul 2026 14:37:32 +0200 Subject: [PATCH 17/17] Internal BouncyCastle's methods imported only once --- Meshtastic/Crypto/XEdDSASigning.cs | 81 ++++++++++++++++++------------ 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 5acce98..7726c10 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -34,6 +34,53 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateSigningKeyPair() return (privateKey, publicKey); } + /* + * Reflected private methods from BouncyCastle's Org.BouncyCastle.Math.EC.Rfc8032.Ed25519 used directly, + * because proper public methods differ in implementation details from those in Meshtastic firmware. + * Using those methods seems less wrong than to re-implement them here. + */ + private static Action? ed25519_ScalarMultBaseEncoded; + private static Action? ed25519_ImplSign; + + private static void Ed25519_ScalarMultBaseEncoded(byte[] k, byte[] r, int rOff) + { + if (ed25519_ScalarMultBaseEncoded == null) + { + var method = typeof(Ed25519).GetMethod("ScalarMultBaseEncoded", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, [typeof(byte[]), typeof(byte[]), typeof(int)]); + if (method == null) throw new InvalidOperationException("Reflected private method \"ScalarMultBaseEncoded\" in \"Org.BouncyCastle.Math.EC.Rfc8032.Ed25519\" not found."); + ed25519_ScalarMultBaseEncoded = (Action)method.CreateDelegate(typeof(Action)); + } + + ed25519_ScalarMultBaseEncoded(k, r, rOff); + } + + private static void Ed25519_ImplSign(IDigest d, byte[] h, byte[] s, byte[] pk, int pkOff, byte[]? ctx, byte phflag, byte[] m, int mOff, int mLen, byte[] sig, int sigOff) + { + if (ed25519_ImplSign == null) + { + var method = typeof(Ed25519) + .GetMethod("ImplSign", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, + [ + typeof(IDigest), + typeof(byte[]), + typeof(byte[]), + typeof(byte[]), + typeof(int), + typeof(byte[]), + typeof(byte), + typeof(byte[]), + typeof(int), + typeof(int), + typeof(byte[]), + typeof(int) + ]); + if (method == null) throw new InvalidOperationException("Reflected private method \"ImplSign\" in \"Org.BouncyCastle.Math.EC.Rfc8032.Ed25519\" not found."); + ed25519_ImplSign = (Action)method.CreateDelegate(typeof(Action)); + } + + ed25519_ImplSign(d, h, s, pk, pkOff, ctx, phflag, m, mOff, mLen, sig, sigOff); + } + private static byte[] GeneratePublicKeyFromPrivateKey(byte[] ed25519PrivateKey) { /* @@ -50,9 +97,7 @@ private static byte[] GeneratePublicKeyFromPrivateKey(byte[] ed25519PrivateKey) */ var ed25519PublicKey = new byte[32]; - typeof(Org.BouncyCastle.Math.EC.Rfc8032.Ed25519) - .GetMethod("ScalarMultBaseEncoded", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, [typeof(byte[]), typeof(byte[]), typeof(int)])! - .Invoke(null, [ed25519PrivateKey, ed25519PublicKey, 0]); + Ed25519_ScalarMultBaseEncoded(ed25519PrivateKey, ed25519PublicKey, 0); return ed25519PublicKey; } @@ -214,35 +259,7 @@ public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKe digest.BlockUpdate(edPrivateKey, 0, 32); digest.DoFinal(h, 0); - typeof(Ed25519) - .GetMethod("ImplSign", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, - [ - typeof(IDigest), - typeof(byte[]), - typeof(byte[]), - typeof(byte[]), - typeof(int), - typeof(byte[]), - typeof(byte), - typeof(byte[]), - typeof(int), - typeof(int), - typeof(byte[]), - typeof(int) - ])! - .Invoke(null, [ - /* IDigest d */ digest, - /* byte[] h */ h, - /* byte[] s */ edPrivateKey /* original argument: s, scalar computed from h */, - /* byte[] pk */ edPublicKey, - /* int pkOff */ 0, - /* byte[] ctx */ null, - /* byte phflag */ (byte)0, - /* byte[] */ message, - /* int mOff*/ 0, - /* int mLen */ message.Length, - /* byte[] sig */ signature, - /* int sigOff */ 0]); + Ed25519_ImplSign(digest, h, edPrivateKey /* original argument: s, scalar computed from h */, edPublicKey, 0, null, 0, message, 0, message.Length, signature, 0); return signature; }