From 7c202ee3d00c20632da9d65fefbb814400664d09 Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Thu, 9 May 2024 17:30:52 +0300 Subject: [PATCH 1/3] Add message signing digest generator --- .../transactions/MessageSigningDigest.java | 47 ++++++++++++ .../MessageSigningDigestTest.java | 76 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/transactions/MessageSigningDigest.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/transactions/MessageSigningDigestTest.java diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/MessageSigningDigest.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/MessageSigningDigest.java new file mode 100644 index 000000000..85db35d76 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/MessageSigningDigest.java @@ -0,0 +1,47 @@ +package com.concordium.sdk.transactions; + +import com.concordium.sdk.crypto.SHA256; +import com.concordium.sdk.types.AccountAddress; +import com.concordium.sdk.types.UInt64; +import lombok.SneakyThrows; +import lombok.val; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class MessageSigningDigest { + + /** + * Creates a digest for signing a regular message with {@link TransactionSigner}. + * + * @param address address of the account signing the message. + * @param message message contents. + * @return 32-byte digest which can be signed by {@link TransactionSigner}. + */ + public static byte[] from(AccountAddress address, byte[] message) { + // When signing a transaction, the 32 bytes of the account address + // gets followed by the account nonce, which is uint64 and by design >= 1. + // In order to sign a regular message and ensure that the user + // does not accidentally sign a transaction, 0 is used as certainly invalid nonce. + // This results in the following sequence: [32 bytes of the address, 8 zero bytes, message]. + val finalMessage = ByteBuffer + .allocate(AccountAddress.BYTES + UInt64.BYTES + message.length) + .put(address.getBytes()) + .put(new UInt64(0).getBytes()) + .put(message) + .array(); + return SHA256.hash(finalMessage); + } + + /** + * Creates a digest for signing a regular plain-text message with {@link TransactionSigner}. + * + * @param address address of the account signing the message. + * @param message plain-text message. + * @return 32-byte digest which can be signed by {@link TransactionSigner}. + */ + @SneakyThrows + public static byte[] from(AccountAddress address, String message) { + return from(address, message.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/transactions/MessageSigningDigestTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/MessageSigningDigestTest.java new file mode 100644 index 000000000..11dec8a5f --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/MessageSigningDigestTest.java @@ -0,0 +1,76 @@ +package com.concordium.sdk.transactions; + +import com.concordium.sdk.types.AccountAddress; +import lombok.SneakyThrows; +import lombok.val; +import org.apache.commons.codec.binary.Hex; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; + +public class MessageSigningDigestTest { + private static final AccountAddress SIGNER_ADDRESS = + AccountAddress.from("3WZE6etUvVp1eyhEtTxqZrQaanTAZnZCHEmZmDyCbCwxnmQuPE"); // Dummy address + + private final Map hexTestVectors = new HashMap<>(); + private final Map plainTestVectors = new HashMap<>(); + + @Before + @SneakyThrows + public void setup() { + hexTestVectors.put( + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", + "e8a1287efd3a0d41e4b7b0a7951c5b67421bd87e659408494c8174c5bef3df09" + ); + hexTestVectors.put( + "d090d0b1d180d0b0d0bad0b0d0b4d0b0d0b1d180d0b0", + "f31c19fa2087a2f04f3cbb91cad10c4081969f29e8407930ce13f50682d9c470" + ); + hexTestVectors.put( + "58e7030200000000e2753e1df53f25ed482ed42f66e69651961783f3e2978a8512a68b9408f30db600e2753e1df53f25ed482ed42f66e69651961783f3e2978a8512a68b9408f30db600e2753e1df53f25ed482ed42f66e69651961783f3e2978a8512a68b9408f30db6e2753e1df53f25ed482ed42f66e69651961783f3e2978a8512a68b9408f30db60300000000000000000000000000000037a2a8e52efad975dbf6580e7734e4f249eaa5ea8a763e934a8671cd7e446499632f567c9321405ce201a0a38615da41efe259ede154ff45ad96cdf860718e79bde07cff72c4d119c644552a8c7f0c413f5cf5390b0ea0458993d6d6374bd90437a2a8e52efad975dbf6580e7734e4f249eaa5ea8a763e934a8671cd7e44649920ccb643bd010000000300616263", + "38b12945d2f06d444b177ba4e4f1038f070646bfd313adbe5b8e1401ece2702c" + ); + hexTestVectors.put( + "", + "c425a3ee7a5706ae8e09884693bd2ba80e843bc7983bac559a3f9edb545acf0c" + ); + + plainTestVectors.put( + "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks", + "6d054bda20eb34b96d7e2e43df2238f33d50982d183e2589a520762b1a766f90" + ); + plainTestVectors.put( + "Абракадабра", + "f31c19fa2087a2f04f3cbb91cad10c4081969f29e8407930ce13f50682d9c470" + ); + plainTestVectors.put( + "", + "c425a3ee7a5706ae8e09884693bd2ba80e843bc7983bac559a3f9edb545acf0c" + ); + } + + @SneakyThrows + @Test + public void testDigestHex() { + for (String messageHex : hexTestVectors.keySet()) { + val message = Hex.decodeHex(messageHex); + val expected = Hex.decodeHex(hexTestVectors.get(messageHex)); + val actual = MessageSigningDigest.from(SIGNER_ADDRESS, message); + assertArrayEquals(expected,actual); + } + } + + @SneakyThrows + @Test + public void testDigestPlain() { + for (String message : plainTestVectors.keySet()) { + val expected = Hex.decodeHex(plainTestVectors.get(message)); + val actual = MessageSigningDigest.from(SIGNER_ADDRESS, message); + assertArrayEquals(expected,actual); + } + } +} From 3f0cf2b9a2274e5509329e37704485cde979f02e Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Fri, 10 May 2024 15:22:57 +0300 Subject: [PATCH 2/3] Add message signing example --- .../concordium/sdk/examples/SignMessage.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/SignMessage.java diff --git a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/SignMessage.java b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/SignMessage.java new file mode 100644 index 000000000..a8a9729a6 --- /dev/null +++ b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/SignMessage.java @@ -0,0 +1,73 @@ +package com.concordium.sdk.examples; + +import com.concordium.sdk.crypto.ed25519.ED25519SecretKey; +import com.concordium.sdk.transactions.Index; +import com.concordium.sdk.transactions.MessageSigningDigest; +import com.concordium.sdk.transactions.SignerEntry; +import com.concordium.sdk.transactions.TransactionSigner; +import com.concordium.sdk.types.AccountAddress; +import lombok.val; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.FileUtils; +import picocli.CommandLine; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.Callable; + +/** + * Signs the given message in the same way the transactions are signed. + */ +@CommandLine.Command(name = "SignMessage", mixinStandardHelpOptions = true) +public class SignMessage implements Callable { + @CommandLine.Option( + names = {"--endpoint"}, + description = "GRPC interface of the node.", + defaultValue = "http://localhost:20001") + private String endpoint; + + @CommandLine.Option( + names = "--hex", + description = "Whether the given file contains HEX-encoded data") + private boolean isHex; + + @CommandLine.Parameters( + index = "0", + description = "A file containing the message" + ) + private Path messageFilePath; + + @Override + public Integer call() throws Exception { + val signer = TransactionSigner.from( + SignerEntry.from( + Index.from(0), + Index.from(0), + ED25519SecretKey.from("7100071c835a0a35e86dccba7ee9d10b89e36d1e596771cdc8ee36a17f7abbf2") + ) + ); + val address = AccountAddress.from("3WZE6etUvVp1eyhEtTxqZrQaanTAZnZCHEmZmDyCbCwxnmQuPE"); + val fileContents = Files.readAllBytes(messageFilePath); + System.out.println(messageFilePath); + val message = (isHex) ? Hex.decodeHex(new String(fileContents)) : fileContents; + val digest = MessageSigningDigest.from(address, message); + val signature = signer.sign(digest); + System.out.println(Hex.encodeHexString( + Objects.requireNonNull( + Objects.requireNonNull(signature.getSignatures().firstEntry()) + .getValue() + .getSignatures() + .firstEntry() + ) + .getValue() + .getBytes() + )); + return 0; + } + + public static void main(String[] args) { + int exitCode = new CommandLine(new SignMessage()).execute(args); + System.exit(exitCode); + } +} From 78946aa2d4614703aa9b6e8381ec09b0446cb646 Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Fri, 10 May 2024 17:40:05 +0300 Subject: [PATCH 3/3] Actualize the changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1775ef20..b5942a124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Unreleased +- Added `MessageSigningDigest` class to generate digests for message signing + ## 7.1.0 - Removed unnecessary `amount` parameter from `InvokeInstanceRequest`. - Added utility functions for converting between `CCDAmount` and `Energy`. Present in utility class `Converter`.