diff --git a/build.gradle b/build.gradle index 96c58baa..fb11cc99 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'jacoco' allprojects { group = 'io.emeraldpay.polkaj' - version = "0.5.1-SNAPSHOT" + version = "0.5.2-SNAPSHOT" repositories { mavenLocal() diff --git a/polkaj-schnorrkel/build.gradle b/polkaj-schnorrkel/build.gradle index ff0ca907..f75b4115 100644 --- a/polkaj-schnorrkel/build.gradle +++ b/polkaj-schnorrkel/build.gradle @@ -13,19 +13,4 @@ task compileRust(type:Exec) { commandLine 'cargo', 'build', '--release', '--target-dir=../../build/rust' } -compileJava.dependsOn(compileRust) - -jar { - from("${buildDir}/rust/release") { - into "native/macos" - include '*.dylib' - } - from("${buildDir}/rust/release") { - into "native/linux" - include '*.so' - } - from("${buildDir}/rust/release") { - into "native/windows" - include '*.dll' - } -} +compileJava.dependsOn(compileRust) \ No newline at end of file diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java new file mode 100644 index 00000000..c1ab2049 --- /dev/null +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java @@ -0,0 +1,59 @@ +package io.emeraldpay.polkaj.merlin; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * A container class to simply hold all the necessary input data (label + messages) to construct the actual + * transcript on Rust's side. Think of this as "the bag of currently necessary arguments". + * The main idea of this class is to be easily portable to Rust using JNI. + * It has no usage on its own on the Java side alone. + */ +@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection", "FieldCanBeLocal"}) +public class TranscriptData { + // INTENTIONALITY: Those fields being of type ArrayList is essential for the JNI mappings on the Rust side + + // HACK: + // Semantically, the internal representation of the domainSeparationLabel should be a single byte[] + // Due to an unresolved issue with robusta (the rust lib) though, + // the field mapping from byte[] doesn't work as expected, so this is the current workaround. + private final ArrayList domainSeparationLabel; + + private final ArrayList labels; + private final ArrayList messages; + + public TranscriptData(byte[] domainSeparationLabel) { + this.domainSeparationLabel = new ArrayList<>(); + this.domainSeparationLabel.add(domainSeparationLabel); + this.labels = new ArrayList<>(); + this.messages = new ArrayList<>(); + } + + /** + * Appends an ASCII encoded string message to the transcript with an ASCII encoded string label. + * @param label the ASCII encoded label + * @param message the ASCII encoded message + */ + public void appendMessage(String label, String message) { + appendMessage(label, message.getBytes(StandardCharsets.US_ASCII)); + } + + /** + * Appends a message to the transcript with an ASCII encoded string label. + * @param label the ASCII encoded label + * @param message the actual message (content) + */ + public void appendMessage(String label, byte[] message) { + appendMessage(label.getBytes(StandardCharsets.US_ASCII), message); + } + + /** + * Appends a message to the transcript. + * @param label the label of the message + * @param message the actual message (content) + */ + public void appendMessage(byte[] label, byte[] message) { + labels.add(label); + messages.add(message); + } +} diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java index 0ce82b1d..44ec85c8 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java @@ -1,18 +1,9 @@ package io.emeraldpay.polkaj.schnorrkel; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.invoke.VarHandle; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.NoSuchAlgorithmException; +import io.emeraldpay.polkaj.merlin.TranscriptData; + import java.security.SecureRandom; import java.util.Arrays; -import java.util.logging.MemoryHandler; /** * Schnorrkel implements Schnorr signature on Ristretto compressed Ed25519 points, as well as related protocols like @@ -141,6 +132,10 @@ public static Schnorrkel getInstance() { */ public abstract Schnorrkel.PublicKey derivePublicKeySoft(Schnorrkel.PublicKey base, Schnorrkel.ChainCode chainCode) throws SchnorrkelException; + public abstract boolean vrfVerify(PublicKey sk, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof); + + public abstract VrfOutputAndProof vrfSign(KeyPair keyPair, TranscriptData transcript); + // ====================== Supporting Classes ====================== /** @@ -248,5 +243,4 @@ public int hashCode() { return Arrays.hashCode(value); } } - } diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java index 3293a17c..51b03ddf 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java @@ -1,5 +1,7 @@ package io.emeraldpay.polkaj.schnorrkel; +import io.emeraldpay.polkaj.merlin.TranscriptData; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -55,6 +57,8 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { return result; } + // ====================== Mapping to the Native Library ====================== + private static native byte[] sign(byte[] publicKey, byte[] secretKey, byte[] message); private static native byte[] keypairFromSeed(byte[] seed); @@ -67,7 +71,9 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { private static native byte[] derivePublicKeySoft(byte[] publicKey, byte[] cc); - // ====================== Mapping to the Native Library ====================== + private static native boolean vrfVerify(byte[] publicKey, TranscriptData transcript, byte[] vrfOutput, byte[] vrfProof); + + private static native byte[] vrfSign(byte[] secretKey, TranscriptData transcript); private static boolean extractAndLoadJNI() throws IOException { // define which of files bundled with Jar to extract @@ -99,7 +105,6 @@ private static boolean extractAndLoadJNI() throws IOException { // extract native lib to the filesystem InputStream lib = Schnorrkel.class.getResourceAsStream(classpathFile); - System.out.println(classpathFile); if (lib == null) { System.err.println("Library " + classpathFile + " is not found in the classpath"); return false; @@ -109,7 +114,7 @@ private static boolean extractAndLoadJNI() throws IOException { Files.copy(lib, target); System.load(target.toFile().getAbsolutePath()); - System.out.println("library " + classpathFile + " is loaded"); + System.out.println("Library " + classpathFile + " is loaded"); // setup JVM to delete files on exit, when possible target.toFile().deleteOnExit(); @@ -170,4 +175,28 @@ public PublicKey derivePublicKeySoft(PublicKey base, ChainCode chainCode) throws return new Schnorrkel.PublicKey(key); } + @Override + public boolean vrfVerify(PublicKey pk, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof) { + return vrfVerify( + pk.getPublicKey(), + transcript, + vrfOutputAndProof.getOutput(), + vrfOutputAndProof.getProof() + ); + } + + @Override + public VrfOutputAndProof vrfSign(KeyPair keyPair, TranscriptData transcript) { + byte[] vrfOutputAndProofBytes = vrfSign(keyPair.getSecretKey(), transcript); + final int OUTPUT_LEN = VrfOutputAndProof.OUTPUT_BYTE_LEN; + final int PROOF_LEN = VrfOutputAndProof.PROOF_BYTE_LEN; + + // effectively, split the array + byte[] vrfOutput = new byte[OUTPUT_LEN]; + byte[] vrfProof = new byte[PROOF_LEN]; + System.arraycopy(vrfOutputAndProofBytes, 0, vrfOutput, 0, OUTPUT_LEN); + System.arraycopy(vrfOutputAndProofBytes, OUTPUT_LEN, vrfProof, 0, PROOF_LEN); + + return VrfOutputAndProof.wrap(vrfOutput, vrfProof); + } } diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java new file mode 100644 index 00000000..bc2f23c1 --- /dev/null +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java @@ -0,0 +1,40 @@ +package io.emeraldpay.polkaj.schnorrkel; + +/** + * Essentially a record to hold the VRF output together with its proof. + */ +public class VrfOutputAndProof { + public static final int OUTPUT_BYTE_LEN = 32; + public static final int PROOF_BYTE_LEN = 64; + + private final byte[] output; + + private final byte[] proof; + + public static VrfOutputAndProof wrap(byte[] output, byte[] proof) { + if (output.length != OUTPUT_BYTE_LEN) { + throw new IllegalArgumentException( + String.format("VRF output must be %d bytes (compressed ristretto point).", OUTPUT_BYTE_LEN)); + } + + if (proof.length != PROOF_BYTE_LEN) { + throw new IllegalArgumentException( + String.format("VRF proof must be %d bytes (compressed ristretto point).", PROOF_BYTE_LEN)); + } + + return new VrfOutputAndProof(output, proof); + } + + private VrfOutputAndProof(byte[] output, byte[] proof) { + this.output = output; + this.proof = proof; + } + + public byte[] getOutput() { + return output; + } + + public byte[] getProof() { + return proof; + } +} diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so index 7e3f2014..57c9fa98 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so and b/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so index f1d92d32..ec185f5b 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so and b/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so index 0245e022..af900662 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so and b/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib b/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib index e4afdd61..edda444e 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib and b/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib differ diff --git a/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib b/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib index e14e0532..6971d03c 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib and b/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib differ diff --git a/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll b/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll index 2821ac40..d3ec9294 100755 Binary files a/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll and b/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll differ diff --git a/polkaj-schnorrkel/src/rust/.gitignore b/polkaj-schnorrkel/src/rust/.gitignore index b354aec7..9f970225 100644 --- a/polkaj-schnorrkel/src/rust/.gitignore +++ b/polkaj-schnorrkel/src/rust/.gitignore @@ -1,2 +1 @@ -Cargo.lock target/ \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/.vscode/settings.json b/polkaj-schnorrkel/src/rust/.vscode/settings.json new file mode 100644 index 00000000..80121080 --- /dev/null +++ b/polkaj-schnorrkel/src/rust/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/Cargo.lock b/polkaj-schnorrkel/src/rust/Cargo.lock new file mode 100644 index 00000000..25a5bada --- /dev/null +++ b/polkaj-schnorrkel/src/rust/Cargo.lock @@ -0,0 +1,767 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + +[[package]] +name = "polkaj-schnorrkel" +version = "0.3.0" +dependencies = [ + "hex", + "jni 0.21.1", + "merlin", + "rand", + "robusta_jni", + "schnorrkel", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "robusta-codegen" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb512b451472948a204452dfad582bdc48d69caacdd3b1b4571d5e3f11707f3" +dependencies = [ + "Inflector", + "darling", + "proc-macro-error", + "proc-macro2", + "quote", + "rand", + "syn 1.0.109", +] + +[[package]] +name = "robusta_jni" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c080146e0cc733697fe500413871142af91bd879641205c2febbe5f982f304e3" +dependencies = [ + "jni 0.19.0", + "paste", + "robusta-codegen", + "static_assertions", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schnorrkel" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] diff --git a/polkaj-schnorrkel/src/rust/Cargo.toml b/polkaj-schnorrkel/src/rust/Cargo.toml index daa3d77f..e8223318 100644 --- a/polkaj-schnorrkel/src/rust/Cargo.toml +++ b/polkaj-schnorrkel/src/rust/Cargo.toml @@ -6,7 +6,9 @@ version = "0.3.0" crate_type = ["cdylib"] [dependencies] -jni = "0.17.0" -schnorrkel = "0.9.1" +robusta_jni = "0.2" +jni = "0.21.1" +schnorrkel = "0.11.4" hex = "0.4.2" -rand = "0.7.3" \ No newline at end of file +rand = "0.8.5" +merlin = "3.0" \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/src/lib.rs b/polkaj-schnorrkel/src/rust/src/lib.rs index 5c895ae0..678d252a 100644 --- a/polkaj-schnorrkel/src/rust/src/lib.rs +++ b/polkaj-schnorrkel/src/rust/src/lib.rs @@ -2,18 +2,27 @@ // Based on https://github.com/polkadot-js/wasm/blob/master/packages/wasm-crypto/src/sr25519.rs // -extern crate jni; extern crate schnorrkel; extern crate hex; extern crate rand; +extern crate robusta_jni; +extern crate merlin; -use jni::JNIEnv; -use jni::objects::{JClass}; -use jni::sys::{jbyteArray, jboolean}; +mod merlin_jni; + +use merlin::Transcript; +use robusta_jni::jni::objects::JObject; +use robusta_jni::jni::JNIEnv; +use robusta_jni::jni::objects::JClass; +use robusta_jni::jni::sys::{jboolean, jbyteArray}; +use schnorrkel::vrf::{VRFInOut, VRFProof, VRFProofBatchable}; use schnorrkel::{SecretKey, PublicKey, Signature, SignatureError, MiniSecretKey, ExpansionMode, Keypair}; use schnorrkel::derive::{ChainCode, CHAIN_CODE_LENGTH, Derivation}; use std::string::String; +use merlin_jni::TranscriptData; +use robusta_jni::convert::TryFromJavaValue; + const SIGNING_CTX: &'static [u8] = b"substrate"; /// ChainCode construction helper @@ -235,4 +244,104 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_der } }; output +} + +#[no_mangle] +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_vrfVerify( + env: JNIEnv, + _class: JClass, + pk_raw: jbyteArray, + transcript_data_raw: JObject, + vrf_output_raw: jbyteArray, + vrf_proof_raw: jbyteArray +) -> jboolean { + let pk_bytes = env.convert_byte_array(pk_raw).expect("Public key bytes not provided."); + + let transcript_data = match TranscriptData::try_from(transcript_data_raw, &env) { + Ok(data) => data, + Err(msg) => { + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg.to_string()).unwrap(); + return false as jboolean; + }, + }; + + let transcript = Transcript::from(transcript_data); + + let vrf_output_bytes = env.convert_byte_array(vrf_output_raw).expect("Vrf output bytes not provided."); + + let vrf_proof_bytes = env.convert_byte_array(vrf_proof_raw).expect("Vrf proof bytes not provided."); + + let output = match vrf_verify(&pk_bytes, transcript, &vrf_output_bytes, &vrf_proof_bytes) { + Ok(_) => true, + Err(SignatureError::EquationFalse) => false, + Err(err) => { + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", err.to_string()).unwrap(); + false + }, + }; + + output as jboolean +} + +fn vrf_verify( + pk_bytes: &[u8], + transcript: Transcript, + vrf_output_bytes: &[u8], + vrf_proof_bytes: &[u8] +) -> Result<(), SignatureError> { + let signing_public_key = schnorrkel::PublicKey::from_bytes(pk_bytes)?; + + // NOTE: + // These `from_bytes`s can only panic if `vrf_output_bytes` or `vrf_proof_bytes` are of the wrong + // length, which is the Java caller's responsibility. In any case, errors are properly accounted for. + let vrf_output = schnorrkel::vrf::VRFPreOut::from_bytes(vrf_output_bytes)?; + let vrf_proof = schnorrkel::vrf::VRFProof::from_bytes(&vrf_proof_bytes)?; + + signing_public_key.vrf_verify(transcript, &vrf_output, &vrf_proof)?; + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_vrfSign( + env: JNIEnv, + _class: JClass, + sk_raw: jbyteArray, + transcript_data_raw: JObject, +) -> jbyteArray { + let sk_bytes = env.convert_byte_array(sk_raw).expect("Secret key bytes not provided."); + + let transcript_data = match TranscriptData::try_from(transcript_data_raw, &env) { + Ok(data) => data, + Err(msg) => { + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg.to_string()).unwrap(); + return *JObject::null(); + }, + }; + + let transcript = Transcript::from(transcript_data); + + match vrf_sign(&sk_bytes, transcript) { + Err(err) => { + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", err.to_string()).unwrap(); + *JObject::null() + }, + Ok((VRFInOut { output, .. }, vrf_proof, _)) => { + let output_bytes = output.to_bytes(); + let proof_bytes = vrf_proof.to_bytes(); + + // HACK: + // Constructing a Java class instance from Rust is a bit of a pain. + // So for now, we're just concatenating the two byte arrays and returning them as one as a hacky serialization workaround. + // If, however, in future we'd want to expand this to a more complex data structure, additional work would be needed. + let output_and_proof: Vec = output_bytes.iter().chain(proof_bytes.iter()).map(|v| *v).collect(); + env.byte_array_from_slice(output_and_proof.as_slice()) + .expect("Couldn't create result") + }, + } +} + +fn vrf_sign(sk_bytes: &[u8], transcript: Transcript) -> Result<(VRFInOut, VRFProof, VRFProofBatchable), SignatureError> { + let sk = SecretKey::from_ed25519_bytes(&sk_bytes)?; + let keypair = sk.to_keypair(); + Ok(keypair.vrf_sign(transcript)) } \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs b/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs new file mode 100644 index 00000000..031e37df --- /dev/null +++ b/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs @@ -0,0 +1,60 @@ +use robusta_jni::bridge; + +pub use self::jni::TranscriptData; + +#[bridge] +pub mod jni { + use robusta_jni::convert::{ + Field, Signature, TryFromJavaValue, + }; + use robusta_jni::jni::objects::AutoLocal; + + #[derive(Signature, TryFromJavaValue)] + #[package(io.emeraldpay.polkaj.merlin)] + pub struct TranscriptData<'env: 'borrow, 'borrow> { + #[allow(dead_code)] + #[instance] + raw: AutoLocal<'env, 'borrow>, + + // HACK: + // Figure out how to make this simply a `Box<[u8]>` instead of a `Vec>`. + // Currently, this is a blocker with byte[] fields; seems to be an issue with robusta itself, perhaps some missing impls... + // see: https://github.com/giovanniberti/robusta/issues/69 + #[field] + pub domainSeparationLabel: Field<'env, 'borrow, Vec>>, + + #[field] + pub labels: Field<'env, 'borrow, Vec>>, + + #[field] + pub messages: Field<'env, 'borrow, Vec>>, + } +} + + +impl <'env, 'borrow> From> for merlin::Transcript { + // SAFETY: + // Since merlin's API requires labels to have a static lifetime, a `Box::leak` invocation is necessary. + // This is safe enough, however, since the boxed values are managed by the Java runtime and will be GC'd there when needed, + // so Rust doesn't have to worry about that (although it wants to). + fn from(value: TranscriptData) -> Self { + // SAFETY: We're purposefully dropping mutability since we want to read only + let domain_separation_label: &[u8] = Box::leak( + value.domainSeparationLabel.get().unwrap() + .first() + .cloned() + .unwrap() + ); + + let mut transcript = merlin::Transcript::new(domain_separation_label); + + let message_labels = value.labels.get().unwrap(); + let messages = value.messages.get().unwrap(); + + for (label, message) in message_labels.iter().cloned().zip(messages.iter().cloned()) { + transcript.append_message(Box::leak(label), message.as_ref()) + } + + transcript + } +} \ No newline at end of file diff --git a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy index ab08023a..1c87e19d 100644 --- a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy +++ b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy @@ -1,5 +1,6 @@ package io.emeraldpay.polkaj.schnorrkel +import io.emeraldpay.polkaj.merlin.TranscriptData import org.apache.commons.codec.binary.Hex import spock.lang.Specification @@ -33,7 +34,7 @@ class SchnorrkelNativeSpec extends Specification { Hex.decodeHex( "28b0" ) - )); + )) then: def t = thrown(SchnorrkelException) t.message.length() > 0 @@ -47,7 +48,7 @@ class SchnorrkelNativeSpec extends Specification { byte[] signature = schnorrkel.sign(msg, key1) def act = schnorrkel.verify(signature, msg, key1) then: - act == true + act } def "Modified signature is invalid"() { @@ -57,24 +58,23 @@ class SchnorrkelNativeSpec extends Specification { byte[] signature = schnorrkel.sign(msg, key1) def initial = schnorrkel.verify(signature, msg, key1) then: - initial == true + initial when: signature[0] = (byte)(signature[0] + 1) def act = schnorrkel.verify(signature, msg, key1) then: - act == false + !act } def "Different signature is invalid"() { setup: byte[] msg = "hello".bytes when: - byte[] signature = schnorrkel.sign(msg, key1) byte[] signature2 = schnorrkel.sign("hello2".bytes, key1) def act = schnorrkel.verify(signature2, msg, key1) then: - act == false + !act } def "Throws error on invalid signature"() { @@ -170,4 +170,73 @@ class SchnorrkelNativeSpec extends Specification { then: Hex.encodeHexString(act.getPublicKey()) == "40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a" } + + // taken from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L90 + def "Test VRF sign and verify"() { + setup: + def signTranscript = new TranscriptData("vrf-test".getBytes()) + def verifyTranscript = new TranscriptData("vrf-test".getBytes()) + + def keyPair = schnorrkel.generateKeyPair() + + def vrfOutputAndProof = schnorrkel.vrfSign(keyPair, signTranscript) + + when: + boolean verified = schnorrkel.vrfVerify(keyPair, verifyTranscript, vrfOutputAndProof) + + then: + verified + } + + // translated from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L172 + def "Test VRF verify from Rust"() { + setup: + byte[] pubKeyBytes = new byte[]{-64, 42, 72, -70, 20, 11, 83, -106, -11, 69, -88, -34, 22, -90, -89, 95, 125, -8, -72, 67, -59, 10, -95, 107, -51, 116, -113, -92, -113, 127, -90, 84} + Schnorrkel.PublicKey pubKey = new Schnorrkel.PublicKey(pubKeyBytes) + + TranscriptData transcript = new TranscriptData("SigningContext".getBytes()) + transcript.appendMessage("", "yo!") + transcript.appendMessage("sign-bytes", "meow") + + byte[] outputBytes = new byte[] {0, 91, 50, 25, -42, 94, 119, 36, 71, -40, 33, -104, 85, -72, 34, 120, 61, -95, -92, -33, 76, 53, 40, -10, 76, 38, -21, -52, 43, 31, -77, 28} + + byte[] cbytes = new byte[] {120, 23, -21, -97, 115, 122, -49, -50, 123, -24, 75, -13, 115, -1, -125, -75, -37, -15, -56, -50, 21, 22, -18, 16, 68, 49, 86, 99, 76, -117, 39, 0} + byte[] sbytes = new byte[] {102, 106, -75, -120, 97, -115, -69, 1, -22, -73, -15, 28, 27, -27, -123, 8, 32, -10, -11, -50, -57, -114, -122, 124, -30, -39, 95, 30, -80, -10, 5, 3} + byte[] proofBytes = new byte[cbytes.length + sbytes.length] + System.arraycopy(cbytes, 0, proofBytes, 0, cbytes.length) + System.arraycopy(sbytes, 0, proofBytes, cbytes.length, sbytes.length) + + when: + boolean verified = schnorrkel.vrfVerify(pubKey, transcript, VrfOutputAndProof.wrap(outputBytes, proofBytes)) + + then: + verified + } + + // NOTE: This test is currently only a sanity check against trivially false positives + // translated from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L149 + def "Test VRF verify invalid proof fails"() { + setup: + def transcript = new TranscriptData("vrf-test".getBytes()) + // Ideally, we'd want this test case to corroborate that "choosing any other than the right scalar would invalidate the proof", + // i.e. we'd only want to change one of the scalars in the proof. + // But since we don't have that granularity on our Java side, for now, + // I've hardcoded this "fake proof", generated from another secret key and another scalar picked at random. + def invalidProof = new byte[]{117, 87, 1, 77, 44, 43, -13, 116, 47, 125, -44, 124, 47, -113, 46, -126, -76, 68, 46, -11, 42, 64, 7, 43, -121, 107, -18, 123, 19, 66, 115, 7, 107, -104, 95, 62, 4, -100, 111, -23, 79, 56, 108, 113, -127, -85, -38, -35, -33, -97, -43, 85, 52, 106, 71, -53, 11, 81, 96, 35, -80, -119, -14, 1} + + def keyPair = schnorrkel.generateKeyPair() + def vrfOutputAndProof = schnorrkel.vrfSign(keyPair, transcript) + + when: + def verifiedValidProof = schnorrkel.vrfVerify(keyPair, transcript, vrfOutputAndProof) + + then: + verifiedValidProof + + when: + def verifiedInvalidProof = schnorrkel.vrfVerify(keyPair, transcript, VrfOutputAndProof.wrap(vrfOutputAndProof.getOutput(), invalidProof)) + + then: + !verifiedInvalidProof + } }