diff --git a/.gitignore b/.gitignore
index 96ef6c0..1e7caa9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-/target
Cargo.lock
+target/
diff --git a/ed25519jni/README.md b/ed25519jni/README.md
new file mode 100644
index 0000000..490c51e
--- /dev/null
+++ b/ed25519jni/README.md
@@ -0,0 +1,49 @@
+# JNI
+Code that provides a [JNI](https://en.wikipedia.org/wiki/Java_Native_Interface)
+for the library is included. Allows any JNI-using language to interact with
+specific `ed25519-zebra` calls and provides a minor analogue for some Rust
+classes, allowing for things like basic sanity checks of certain values. Tests
+written in Scala have also been included.
+
+## Compilation / Library Usage
+To build the JNI code, there are several steps. The exact path forward depends
+on the user's preferred deployment method. No matter what, the following steps
+must be performed at the beginning.
+
+- Run `cargo build` in the root directory. This generates the core Rust code.
+- Run `cargo build` in the `ed25519jni/rust` subdirectory. This generates the Rust
+ glue code libraries (`libed25519jni.a` and `libed25519jni.{so/dylib}`).
+
+From here, there are two deployment methods: Direct library usage and JARs.
+
+### JAR
+
+
+It's possible to generate a JAR that can be loaded into a project via
+[SciJava's NativeLoader](https://javadoc.scijava.org/SciJava/org/scijava/nativelib/NativeLoader.html),
+along with the Java JNI interface file. There are two exta steps to perform
+after the mandatory compilation steps.
+
+- Run `jni_jar_prereq.sh` from the `ed25519/scripts` subdirectory. This performs
+ some JAR setup steps.
+- Run `sbt clean publishLocal` from the `ed25519jni/jvm` subdirectory. This
+ generates the final `ed25519jni.jar` file.
+
+### Direct library usage
+(NOTE: Future work will better accommodate this option. For now, users will have
+to develop their own solutions.)
+
+Use a preferred method to load the Rust core and JNI libraries directly as
+needed. If necessary, include the JNI Java files too.
+
+## Testing
+Run `sbt test` from the `ed25519jni/jvm` directory. Note that, in order to run
+the tests, the [JAR compilation method](#jar) must be executed first.
+
+## Capabilities
+Among other things, the JNI code can perform the following actions.
+
+* Generate a random 32 byte signing key seed.
+* Generate a 32 byte verification key from a signing key seed.
+* Sign arbitrary data with a signing key seed.
+* Verify a signature for arbitrary data with verification key bytes (32 bytes).
diff --git a/ed25519jni/jvm/.gitignore b/ed25519jni/jvm/.gitignore
new file mode 100644
index 0000000..8b26ba8
--- /dev/null
+++ b/ed25519jni/jvm/.gitignore
@@ -0,0 +1 @@
+/natives/
diff --git a/ed25519jni/jvm/build.sbt b/ed25519jni/jvm/build.sbt
new file mode 100644
index 0000000..02f64ba
--- /dev/null
+++ b/ed25519jni/jvm/build.sbt
@@ -0,0 +1,25 @@
+organization := "org.zfnd"
+
+name := "ed25519jni"
+
+version := "0.0.4-JNI-DEV"
+
+scalaVersion := "2.12.10"
+
+scalacOptions ++= Seq("-Xmax-classfile-name", "140")
+
+autoScalaLibrary := false // exclude scala-library from dependencies
+
+crossPaths := false // drop off Scala suffix from artifact names.
+
+libraryDependencies ++= Deps.ed25519jni
+
+unmanagedResourceDirectories in Compile += baseDirectory.value / "natives"
+
+publishArtifact := true
+
+javacOptions in (Compile,doc) ++= Seq(
+ "-windowtitle", "JNI bindings for ed25519-zebra"
+)
+
+testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
diff --git a/ed25519jni/jvm/project/Deps.scala b/ed25519jni/jvm/project/Deps.scala
new file mode 100644
index 0000000..099b788
--- /dev/null
+++ b/ed25519jni/jvm/project/Deps.scala
@@ -0,0 +1,24 @@
+import sbt._
+
+object Deps {
+
+ object V {
+ val nativeLoaderV = "2.3.4"
+ val scalaTest = "3.0.9"
+ val slf4j = "1.7.30"
+ }
+
+ object Test {
+ val nativeLoader = "org.scijava" % "native-lib-loader" % V.nativeLoaderV
+ val scalaTest = "org.scalatest" %% "scalatest" % V.scalaTest % "test"
+ val slf4jApi = "org.slf4j" % "slf4j-api" % V.slf4j
+ val slf4jSimple = "org.slf4j" % "slf4j-simple" % V.slf4j % "test"
+ }
+
+ val ed25519jni = List(
+ Test.nativeLoader,
+ Test.scalaTest,
+ Test.slf4jApi,
+ Test.slf4jSimple,
+ )
+}
diff --git a/ed25519jni/jvm/project/build.properties b/ed25519jni/jvm/project/build.properties
new file mode 100644
index 0000000..d91c272
--- /dev/null
+++ b/ed25519jni/jvm/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.6
diff --git a/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java
new file mode 100644
index 0000000..b8b0792
--- /dev/null
+++ b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java
@@ -0,0 +1,136 @@
+package org.zfnd.ed25519;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import org.scijava.nativelib.NativeLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Ed25519Interface {
+ private static final Logger logger;
+ private static final boolean enabled;
+
+ static {
+ logger = LoggerFactory.getLogger(Ed25519Interface.class);
+ boolean isEnabled = true;
+
+ try {
+ NativeLoader.loadLibrary("ed25519jni");
+ } catch (java.io.IOException | UnsatisfiedLinkError e) {
+ logger.error("Could not find ed25519jni - Interface is not enabled - ", e);
+ isEnabled = false;
+ }
+ enabled = isEnabled;
+ }
+
+ /**
+ * Helper method to determine whether the Ed25519 Rust backend is loaded and
+ * available.
+ *
+ * @return whether the Ed25519 Rust backend is enabled
+ */
+ public static boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Generate a new Ed25519 signing key seed and check the results for validity. This
+ * code is valid but not canonical. If the Rust code ever adds restrictions on which
+ * values are allowed, this code will have to stay in sync.
+ *
+ * @param rng An initialized, secure RNG
+ * @return sks 32 byte signing key seed
+ */
+ private static byte[] genSigningKeySeedFromJava(SecureRandom rng) {
+ byte[] seedBytes = new byte[SigningKeySeed.BYTE_LENGTH];
+
+ do {
+ rng.nextBytes(seedBytes);
+ } while(!SigningKeySeed.bytesAreValid(seedBytes));
+
+ return seedBytes;
+ }
+
+ /**
+ * Public frontend to use when generating a signing key seed.
+ *
+ * @param rng source of entropy for key material
+ * @return instance of SigningKeySeed containing an EdDSA signing key seed
+ */
+ public static SigningKeySeed genSigningKeySeed(SecureRandom rng) {
+ return new SigningKeySeed(genSigningKeySeedFromJava(rng));
+ }
+
+ /**
+ * Check if verification key bytes for a verification key are valid.
+ *
+ * @param vk_bytes 32 byte verification key bytes to verify
+ * @return true if valid, false if not
+ */
+ public static native boolean checkVerificationKeyBytes(byte[] vk_bytes);
+
+ /**
+ * Get verification key bytes from a signing key seed.
+ *
+ * @param sk_seed_bytes 32 byte signing key seed
+ * @return 32 byte verification key
+ * @throws RuntimeException on error in libed25519
+ */
+ private static native byte[] getVerificationKeyBytes(byte[] sk_seed_bytes);
+
+ /**
+ * Get verification key bytes from a signing key seed.
+ *
+ * @param seed signing key seed
+ * @return verification key bytes
+ */
+ public static VerificationKeyBytes getVerificationKeyBytes(SigningKeySeed seed) {
+ return new VerificationKeyBytes(getVerificationKeyBytes(seed.getSigningKeySeed()));
+ }
+
+ /**
+ * Creates a signature on msg using the given signing key.
+ *
+ * @param sk_seed_bytes 32 byte signing key seed
+ * @param msg Message of arbitrary length to be signed
+ * @return signature data
+ * @throws RuntimeException on error in libed25519
+ */
+ private static native byte[] sign(byte[] sk_seed_bytes, byte[] msg);
+
+ /**
+ * Creates a signature on message using the given signing key.
+ *
+ * @param seed signing key seed
+ * @param message Message of arbitrary length to be signed
+ * @return signature data
+ * @throws RuntimeException on error in libed25519
+ */
+ public static Signature sign(SigningKeySeed seed, byte[] message) {
+ return new Signature(sign(seed.getSigningKeySeed(), message));
+ }
+
+ /**
+ * Verifies a purported `signature` on the given `msg`.
+ *
+ * @param vk_bytes 32 byte verification key bytes
+ * @param sig 64 byte signature to be verified
+ * @param msg Message of arbitrary length to be signed
+ * @return true if verified, false if not
+ * @throws RuntimeException on error in libed25519
+ */
+ private static native boolean verify(byte[] vk_bytes, byte[] sig, byte[] msg);
+
+ /**
+ * Verifies a purported `signature` on the given `message` with `verificationKey`.
+ *
+ * @param verificationKey verification key bytes
+ * @param signature 64 byte signature to be verified
+ * @param message message of arbitrary length to be signed
+ * @return true if verified, false if not
+ * @throws RuntimeException on error in libed25519
+ */
+ public static boolean verify(VerificationKeyBytes verificationKey, Signature signature, byte[] message) {
+ return verify(verificationKey.getVerificationKeyBytes(), signature.getSignatureBytes(), message);
+ }
+}
diff --git a/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java
new file mode 100644
index 0000000..00f1eea
--- /dev/null
+++ b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java
@@ -0,0 +1,94 @@
+package org.zfnd.ed25519;
+
+import java.util.Arrays;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Java wrapper class for signatures that performs some sanity checking.
+ */
+public class Signature {
+ public static final int COMPONENT_LENGTH = 32;
+ public static final int SIGNATURE_LENGTH = 2 * COMPONENT_LENGTH;
+ private static final Logger logger = LoggerFactory.getLogger(Signature.class);
+
+ private byte[] rBytes;
+ private byte[] sBytes;
+ private byte[] completeSignature;
+
+ // Don't bother with an expensive, literal check. Just ensure the format's correct.
+ static boolean bytesAreValid(final byte[] signature) {
+ return (signature.length == (SIGNATURE_LENGTH));
+ }
+
+ Signature(final byte[] sig) {
+ // package protected constructor
+ // assumes valid values from us or underlying library and that the caller will not mutate them
+ rBytes = Arrays.copyOfRange(sig, 0, COMPONENT_LENGTH);
+ sBytes = Arrays.copyOfRange(sig, COMPONENT_LENGTH, SIGNATURE_LENGTH);
+
+ // Cache the complete signature array instead of rebuilding when requested.
+ completeSignature = new byte[SIGNATURE_LENGTH];
+ System.arraycopy(rBytes, 0, completeSignature, 0, COMPONENT_LENGTH);
+ System.arraycopy(sBytes, 0, completeSignature, COMPONENT_LENGTH, COMPONENT_LENGTH);
+ }
+
+ /**
+ * @return a copy of the complete signature
+ */
+ public byte[] getSignatureBytesCopy() {
+ return completeSignature.clone();
+ }
+
+ byte[] getSignatureBytes() {
+ return completeSignature;
+ }
+
+ /**
+ * Optionally convert bytes into a verification key wrapper.
+ *
+ * @param bytes untrusted, unvalidated bytes that may be an encoding of a verification key
+ * @return optionally a verification key wrapper, if bytes are valid
+ */
+ public static Optional fromBytes(final byte[] bytes) {
+ if (bytesAreValid(bytes)) {
+ return Optional.of(new Signature(bytes));
+ }
+ else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Convert bytes into a verification key wrapper.
+ *
+ * @param bytes bytes that are expected be an encoding of a verification key
+ * @return a verification key wrapper, if bytes are valid
+ * @throws IllegalArgumentException if bytes are invalid
+ */
+ public static Signature fromBytesOrThrow(final byte[] bytes) {
+ return fromBytes(bytes)
+ .orElseThrow(() -> new IllegalArgumentException("Expected " + (SIGNATURE_LENGTH) + " bytes that encode a signature!"));
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other == this) {
+ return true;
+ } else if (other instanceof Signature) {
+ final Signature that = (Signature) other;
+ return Arrays.equals(that.rBytes, this.rBytes) &&
+ Arrays.equals(that.sBytes, this.sBytes);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 23 * Arrays.hashCode(rBytes);
+ h = 23 * (h + Arrays.hashCode(sBytes));
+ return h;
+ }
+}
diff --git a/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/SigningKeySeed.java b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/SigningKeySeed.java
new file mode 100644
index 0000000..f0acbd7
--- /dev/null
+++ b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/SigningKeySeed.java
@@ -0,0 +1,76 @@
+package org.zfnd.ed25519;
+
+import java.util.Arrays;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Java wrapper class for signing key seeds that performs some sanity checking.
+ */
+public class SigningKeySeed {
+ public static final int BYTE_LENGTH = 32;
+ private static final Logger logger = LoggerFactory.getLogger(SigningKeySeed.class);
+
+ private byte[] seed;
+
+ // Determining if bytes are valid is pretty trivial. Rust code not needed.
+ static boolean bytesAreValid(final byte[] seedBytes) {
+ if(seedBytes.length == BYTE_LENGTH) {
+ for (int b = 0; b < BYTE_LENGTH; b++) {
+ if (seedBytes[b] != 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ SigningKeySeed(final byte[] seed) {
+ // package protected constructor
+ // assumes valid values from us or underlying library and that the caller will not mutate them
+ this.seed = seed;
+ }
+
+ /**
+ * @return a copy of the wrapped bytes
+ */
+ public byte[] getSigningKeySeedCopy() {
+ return seed.clone();
+ }
+
+ byte[] getSigningKeySeed() {
+ return seed;
+ }
+
+ /**
+ * Optionally convert bytes into a signing key seed wrapper.
+ *
+ * @param bytes untrusted, unvalidated bytes that may be a valid signing key seed
+ * @return optionally a signing key seed wrapper, if bytes are valid
+ */
+ public static Optional fromBytes(final byte[] bytes) {
+ // input is mutable and from untrusted source, so take a copy
+ final byte[] cloneBytes = bytes.clone();
+
+ if (bytesAreValid(cloneBytes)) {
+ return Optional.of(new SigningKeySeed(cloneBytes));
+ }
+ else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Convert bytes into a signing key seed wrapper.
+ *
+ * @param bytes bytes that are expected be a valid signing key seed
+ * @return a signing key seed wrapper, if bytes are valid
+ * @throws IllegalArgumentException if bytes are invalid
+ */
+ public static SigningKeySeed fromBytesOrThrow(final byte[] bytes) {
+ return fromBytes(bytes)
+ .orElseThrow(() -> new IllegalArgumentException("Expected " + BYTE_LENGTH + " bytes where not all are zero!"));
+ }
+}
diff --git a/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/VerificationKeyBytes.java b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/VerificationKeyBytes.java
new file mode 100644
index 0000000..60f91ae
--- /dev/null
+++ b/ed25519jni/jvm/src/main/java/org/zfnd/ed25519/VerificationKeyBytes.java
@@ -0,0 +1,85 @@
+package org.zfnd.ed25519;
+
+import java.util.Arrays;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Java wrapper class for verification key bytes that performs some sanity checking.
+ */
+public class VerificationKeyBytes {
+ public static final int BYTE_LENGTH = 32;
+ private static final Logger logger = LoggerFactory.getLogger(VerificationKeyBytes.class);
+
+ private byte[] vkb;
+
+ // Determining if bytes are valid is complicated. Call into Rust.
+ static boolean bytesAreValid(final byte[] verificationKeyBytes) {
+ return (verificationKeyBytes.length == BYTE_LENGTH) && Ed25519Interface.checkVerificationKeyBytes(verificationKeyBytes);
+ }
+
+ VerificationKeyBytes(final byte[] verificationKeyBytes) {
+ // package protected constructor
+ // assumes valid values from us or underlying library and that the caller will not mutate them
+ this.vkb = verificationKeyBytes;
+ }
+
+ /**
+ * @return a copy of the wrapped bytes
+ */
+ public byte[] getVerificationKeyBytesCopy() {
+ return vkb.clone();
+ }
+
+ byte[] getVerificationKeyBytes() {
+ return vkb;
+ }
+
+ /**
+ * Optionally convert bytes into a verification key wrapper.
+ *
+ * @param bytes untrusted, unvalidated bytes that may be an encoding of a verification key
+ * @return optionally a verification key wrapper, if bytes are valid
+ */
+ public static Optional fromBytes(final byte[] bytes) {
+ // input is mutable and from untrusted source, so take a copy
+ final byte[] cloneBytes = bytes.clone();
+
+ if (bytesAreValid(cloneBytes)) {
+ return Optional.of(new VerificationKeyBytes(cloneBytes));
+ }
+ else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Convert bytes into a verification key wrapper.
+ *
+ * @param bytes bytes that are expected be an encoding of a verification key
+ * @return a verification key wrapper, if bytes are valid
+ * @throws IllegalArgumentException if bytes are invalid
+ */
+ public static VerificationKeyBytes fromBytesOrThrow(final byte[] bytes) {
+ return fromBytes(bytes)
+ .orElseThrow(() -> new IllegalArgumentException("Expected " + BYTE_LENGTH + " bytes that encode a verification key!"));
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other == this) {
+ return true;
+ } else if (other instanceof VerificationKeyBytes) {
+ final VerificationKeyBytes that = (VerificationKeyBytes) other;
+ return Arrays.equals(that.vkb, this.vkb);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return 23 * Arrays.hashCode(this.vkb);
+ }
+}
diff --git a/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/Ed25519InterfaceTest.scala b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/Ed25519InterfaceTest.scala
new file mode 100644
index 0000000..a89dda2
--- /dev/null
+++ b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/Ed25519InterfaceTest.scala
@@ -0,0 +1,68 @@
+package org.zfnd.ed25519
+
+import java.math.BigInteger
+import java.security.SecureRandom
+import org.scalatest.{ FlatSpec, MustMatchers }
+
+class Ed25519InterfaceTest extends FlatSpec with MustMatchers {
+ private val RANDOM = new SecureRandom
+
+ private def convertBytesToHex(bytes: Seq[Byte]): String = {
+ val sb = new StringBuilder
+ for (b <- bytes) {
+ sb.append(String.format("%02x", Byte.box(b)))
+ }
+ sb.toString
+ }
+
+ it must "initialize the Ed25519 interface" in {
+ Ed25519Interface.isEnabled mustBe true
+ }
+
+ it must "get a private key" in {
+ val sks = Ed25519Interface.genSigningKeySeed(RANDOM)
+ val sksValue = BigInt(convertBytesToHex(sks.getSigningKeySeed), 16)
+ sksValue must not be BigInteger.ZERO
+ }
+
+ it must "sign and verify data" in {
+ val sks = Ed25519Interface.genSigningKeySeed(RANDOM)
+ val vkb = Ed25519Interface.getVerificationKeyBytes(sks)
+
+ val m = new Array[Byte](32)
+ RANDOM.nextBytes(m)
+ val rustSig = Ed25519Interface.sign(sks, m)
+ Ed25519Interface.verify(vkb, rustSig, m) mustBe (true)
+ }
+
+ it must "reject bad signing key seeds" in {
+ val m = new Array[Byte](32) // 0x0000....
+ val sks = SigningKeySeed.fromBytes(m)
+ sks.isPresent mustBe false
+ }
+
+ it must "reject bad verification key bytes" in {
+ val vkbValue = BigInt("9000000000000000000000000000000000000000000000000000000000000000", 16)
+ var vkb = VerificationKeyBytes.fromBytes(vkbValue.toByteArray)
+ vkb.isPresent mustBe false
+ }
+
+ // Included to deterministically confirm that JNI usage still leads to correct
+ // results. See Sect. 7.1 of RFC 8032.
+ it must "match RFC 8032 test vector data" in {
+ val sksValue = BigInt("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", 16)
+ val sks = new SigningKeySeed(sksValue.toByteArray)
+ val vkb = Ed25519Interface.getVerificationKeyBytes(sks)
+ convertBytesToHex(vkb.getVerificationKeyBytes) mustBe("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c")
+
+ val msg: Array[Byte] = Array(114.toByte) // 0x72
+ val sig = Ed25519Interface.sign(sks, msg)
+ convertBytesToHex(sig.getSignatureBytes) mustBe("92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00")
+
+ // fromBytesOrThrow() sanity checks.
+ val sks2 = SigningKeySeed.fromBytesOrThrow(sks.getSigningKeySeed)
+ convertBytesToHex(sks2.getSigningKeySeed) mustBe("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb")
+ val vkb2 = VerificationKeyBytes.fromBytesOrThrow(vkb.getVerificationKeyBytes)
+ convertBytesToHex(vkb2.getVerificationKeyBytes) mustBe("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c")
+ }
+}
diff --git a/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/SignatureTest.scala b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/SignatureTest.scala
new file mode 100644
index 0000000..26934f5
--- /dev/null
+++ b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/SignatureTest.scala
@@ -0,0 +1,49 @@
+package org.zfnd.ed25519
+
+import java.security.SecureRandom
+import org.scalatest.{ FlatSpec, MustMatchers }
+import scala.collection.mutable.HashSet
+
+class SignatureTest extends FlatSpec with MustMatchers {
+ private val RANDOM = new SecureRandom()
+
+ it must "properly compare Signature objects" in {
+ val sig1 = new Array[Byte](Signature.SIGNATURE_LENGTH);
+ do {
+ RANDOM.nextBytes(sig1);
+ } while(!Signature.bytesAreValid(sig1));
+
+ val sig2 = new Array[Byte](Signature.SIGNATURE_LENGTH);
+ do {
+ RANDOM.nextBytes(sig2);
+ } while(!Signature.bytesAreValid(sig2));
+
+ val sigObj1 = Signature.fromBytesOrThrow(sig1);
+ val sigObj2 = Signature.fromBytesOrThrow(sig1);
+ val sigObj3 = Signature.fromBytesOrThrow(sig2);
+ sigObj1 == sigObj2 mustBe true
+ sigObj2 == sigObj3 mustBe false
+ }
+
+ it must "reject illegal Signature bytes" in {
+ val sig = new Array[Byte](Signature.COMPONENT_LENGTH);
+ RANDOM.nextBytes(sig);
+
+ val sigObj = Signature.fromBytes(sig)
+ sigObj.isPresent() mustBe false
+ }
+
+ it must "properly handle Signatures in hashed data structures" in {
+ val sig = new Array[Byte](Signature.SIGNATURE_LENGTH);
+ do {
+ RANDOM.nextBytes(sig);
+ } while(!Signature.bytesAreValid(sig));
+
+ val sigObj1 = Signature.fromBytesOrThrow(sig);
+ val sigObj2 = Signature.fromBytesOrThrow(sig);
+
+ val sigSet: HashSet[Signature] = HashSet(sigObj1, sigObj2);
+ sigSet.size must be(1);
+ sigSet.contains(Signature.fromBytesOrThrow(sig)) mustBe true
+ }
+}
diff --git a/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/VerificationKeyBytesTest.scala b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/VerificationKeyBytesTest.scala
new file mode 100644
index 0000000..8ed19c8
--- /dev/null
+++ b/ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/VerificationKeyBytesTest.scala
@@ -0,0 +1,48 @@
+package org.zfnd.ed25519
+
+import java.security.SecureRandom
+import org.scalatest.{ FlatSpec, MustMatchers }
+import scala.collection.mutable.HashSet
+
+class VerificationKeyBytesTest extends FlatSpec with MustMatchers {
+ private val RANDOM = new SecureRandom()
+
+ it must "properly compare VerificationKeyBytes objects" in {
+ val vkb1 = new Array[Byte](VerificationKeyBytes.BYTE_LENGTH)
+ do {
+ RANDOM.nextBytes(vkb1)
+ } while(!VerificationKeyBytes.bytesAreValid(vkb1))
+
+ val vkb2 = new Array[Byte](VerificationKeyBytes.BYTE_LENGTH)
+ do {
+ RANDOM.nextBytes(vkb2)
+ } while(!VerificationKeyBytes.bytesAreValid(vkb2))
+
+ val vkbObj1 = new VerificationKeyBytes(vkb1)
+ val vkbObj2 = new VerificationKeyBytes(vkb1)
+ val vkbObj3 = new VerificationKeyBytes(vkb2)
+ vkbObj1 == vkbObj2 mustBe true
+ vkbObj2 == vkbObj3 mustBe false
+ }
+
+ it must "properly handle VerificationKeyBytes in hashed data structures" in {
+ val vkb = new Array[Byte](VerificationKeyBytes.BYTE_LENGTH)
+ do {
+ RANDOM.nextBytes(vkb)
+ } while(!VerificationKeyBytes.bytesAreValid(vkb))
+
+ val vkbObj1 = new VerificationKeyBytes(vkb)
+ val vkbObj2 = new VerificationKeyBytes(vkb)
+
+ val vkbSet: HashSet[VerificationKeyBytes] = HashSet(vkbObj1, vkbObj2)
+ vkbSet.size must be(1)
+ vkbSet.contains(new VerificationKeyBytes(vkb)) mustBe true
+ }
+
+ it must "reject bad VerificationKeyBytes creation attempts via fromBytes()" in {
+ val vkb1 = new Array[Byte](2 * VerificationKeyBytes.BYTE_LENGTH)
+ RANDOM.nextBytes(vkb1)
+ val vkbObj1 = VerificationKeyBytes.fromBytes(vkb1)
+ vkbObj1.isPresent() mustBe false
+ }
+}
diff --git a/ed25519jni/project/build.properties b/ed25519jni/project/build.properties
new file mode 100644
index 0000000..c06db1b
--- /dev/null
+++ b/ed25519jni/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.5
diff --git a/ed25519jni/rust/Cargo.toml b/ed25519jni/rust/Cargo.toml
new file mode 100644
index 0000000..0b0697c
--- /dev/null
+++ b/ed25519jni/rust/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ed25519jni"
+version = "0.0.4-JNI-DEV"
+authors = ["Douglas Roark "]
+license = "MIT OR Apache-2.0"
+publish = false
+edition = "2018"
+
+[dependencies]
+ed25519-zebra = { path = "../../", version = "2.2.0" }
+failure = "0.1.8"
+jni = "0.18.0"
+
+[lib]
+name = "ed25519jni"
+path = "src/lib.rs"
+crate-type = ["staticlib", "cdylib"]
diff --git a/ed25519jni/rust/src/lib.rs b/ed25519jni/rust/src/lib.rs
new file mode 100644
index 0000000..70efa88
--- /dev/null
+++ b/ed25519jni/rust/src/lib.rs
@@ -0,0 +1,94 @@
+use ed25519_zebra::{Signature, SigningKey, VerificationKey, VerificationKeyBytes,};
+use jni::{objects::JClass, sys::{jboolean, jbyteArray}, JNIEnv,};
+use std::{convert::TryFrom, panic, ptr,};
+
+mod utils;
+
+use crate::utils::exception::unwrap_exc_or;
+
+#[no_mangle]
+pub extern "system" fn Java_org_zfnd_ed25519_Ed25519Interface_checkVerificationKeyBytes(
+ env: JNIEnv<'_>,
+ _: JClass<'_>,
+ vk_bytes: jbyteArray,
+) -> jboolean {
+ let mut vkb = [0u8; 32];
+ vkb.copy_from_slice(&env.convert_byte_array(vk_bytes).unwrap());
+
+ let vkb_result = VerificationKeyBytes::try_from(VerificationKeyBytes::from(vkb));
+ vkb_result.is_ok() as _
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_zfnd_ed25519_Ed25519Interface_getVerificationKeyBytes(
+ env: JNIEnv<'_>,
+ _: JClass<'_>,
+ sk_seed_bytes: jbyteArray,
+) -> jbyteArray {
+ let res = panic::catch_unwind(|| {
+ let mut seed_data = [0u8; 32];
+ seed_data.copy_from_slice(&env.convert_byte_array(sk_seed_bytes).unwrap());
+ let sk = SigningKey::from(seed_data);
+ let pkb = VerificationKeyBytes::from(&sk);
+ let pkb_array: [u8; 32] = pkb.into();
+
+ Ok(env.byte_array_from_slice(&pkb_array).unwrap())
+ });
+ unwrap_exc_or(&env, res, ptr::null_mut())
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_zfnd_ed25519_Ed25519Interface_sign(
+ env: JNIEnv<'_>,
+ _: JClass<'_>,
+ sk_seed_bytes: jbyteArray,
+ msg: jbyteArray,
+) -> jbyteArray {
+ let res = panic::catch_unwind(|| {
+ let mut seed_data = [0u8; 32];
+ seed_data.copy_from_slice(&env.convert_byte_array(sk_seed_bytes).unwrap());
+ let sk = SigningKey::from(seed_data);
+
+ let msg = {
+ let mut data = vec![];
+ data.extend_from_slice(&env.convert_byte_array(msg).unwrap());
+ data
+ };
+
+ let signature = {
+ let mut data = [0u8; 64];
+ data.copy_from_slice(&<[u8; 64]>::from(sk.sign(&msg)));
+ data
+ };
+
+ Ok(env.byte_array_from_slice(&signature).unwrap())
+ });
+ unwrap_exc_or(&env, res, ptr::null_mut())
+}
+
+#[no_mangle]
+pub extern "system" fn Java_org_zfnd_ed25519_Ed25519Interface_verify(
+ env: JNIEnv<'_>,
+ _: JClass<'_>,
+ vk_bytes: jbyteArray,
+ signature: jbyteArray,
+ msg: jbyteArray,
+) -> jboolean {
+ let mut vk_data = [0u8; 32];
+ vk_data.copy_from_slice(&env.convert_byte_array(vk_bytes).unwrap());
+
+ let mut sigdata = [0u8; 64];
+ sigdata.copy_from_slice(&env.convert_byte_array(signature).unwrap());
+ let signature = Signature::from(sigdata);
+
+ let msg = {
+ let mut data = vec![];
+ data.extend_from_slice(&env.convert_byte_array(msg).unwrap());
+ data
+ };
+
+ let vkb = VerificationKeyBytes::try_from(VerificationKeyBytes::from(vk_data)).unwrap();
+ let vk = VerificationKey::try_from(vkb).unwrap();
+ let resbool = vk.verify(&signature, &msg).is_ok();
+ resbool as _
+}
diff --git a/ed25519jni/rust/src/utils.rs b/ed25519jni/rust/src/utils.rs
new file mode 100644
index 0000000..660f4bc
--- /dev/null
+++ b/ed25519jni/rust/src/utils.rs
@@ -0,0 +1 @@
+pub(crate) mod exception;
diff --git a/ed25519jni/rust/src/utils/exception.rs b/ed25519jni/rust/src/utils/exception.rs
new file mode 100644
index 0000000..0c4d325
--- /dev/null
+++ b/ed25519jni/rust/src/utils/exception.rs
@@ -0,0 +1,115 @@
+// Copyright 2018 The Exonum Team
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use failure::Error;
+use jni::JNIEnv;
+use std::any::Any;
+use std::thread;
+
+type ExceptionResult = thread::Result>;
+
+// Returns value or "throws" exception. `error_val` is returned, because exception will be thrown
+// at the Java side. So this function should be used only for the `panic::catch_unwind` result.
+pub fn unwrap_exc_or(env: &JNIEnv, res: ExceptionResult, error_val: T) -> T {
+ match res {
+ Ok(Ok(val)) => val,
+ Ok(Err(jni_error)) => {
+ // Do nothing if there is a pending Java-exception that will be thrown
+ // automatically by the JVM when the native method returns.
+ if !env.exception_check().unwrap() {
+ // Throw a Java exception manually in case of an internal error.
+ throw(env, &jni_error.to_string())
+ }
+ error_val
+ }
+ Err(ref e) => {
+ throw(env, &any_to_string(e));
+ error_val
+ }
+ }
+}
+
+// Calls a corresponding `JNIEnv` method, so exception will be thrown when execution returns to
+// the Java side.
+fn throw(env: &JNIEnv, description: &str) {
+ // We cannot throw exception from this function, so errors should be written in log instead.
+ let exception = match env.find_class("java/lang/RuntimeException") {
+ Ok(val) => val,
+ Err(e) => {
+ eprintln!(
+ "Unable to find 'RuntimeException' class: {}",
+ e
+ );
+ return;
+ }
+ };
+ if let Err(e) = env.throw_new(exception, description) {
+ eprintln!(
+ "Unable to find 'RuntimeException' class: {}",
+ e
+ );
+ }
+}
+
+// Tries to get meaningful description from panic-error.
+pub fn any_to_string(any: &Box) -> String {
+ if let Some(s) = any.downcast_ref::<&str>() {
+ s.to_string()
+ } else if let Some(s) = any.downcast_ref::() {
+ s.clone()
+ } else if let Some(error) = any.downcast_ref::>() {
+ error.to_string()
+ } else {
+ "Unknown error occurred".to_string()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::error::Error;
+ use std::panic;
+
+ #[test]
+ fn str_any() {
+ let string = "Static string (&str)";
+ let error = panic_error(string);
+ assert_eq!(string, any_to_string(&error));
+ }
+
+ #[test]
+ fn string_any() {
+ let string = "Owned string (String)".to_owned();
+ let error = panic_error(string.clone());
+ assert_eq!(string, any_to_string(&error));
+ }
+
+ #[test]
+ fn box_error_any() {
+ let error: Box = Box::new("e".parse::().unwrap_err());
+ let description = error.to_string();
+ let error = panic_error(error);
+ assert_eq!(description, any_to_string(&error));
+ }
+
+ #[test]
+ fn unknown_any() {
+ let error = panic_error(1);
+ assert_eq!("Unknown error occurred", any_to_string(&error));
+ }
+
+ fn panic_error(val: T) -> Box {
+ panic::catch_unwind(panic::AssertUnwindSafe(|| panic!(val))).unwrap_err()
+ }
+}
diff --git a/ed25519jni/scripts/jni_jar_prereq.sh b/ed25519jni/scripts/jni_jar_prereq.sh
new file mode 100755
index 0000000..13f64d7
--- /dev/null
+++ b/ed25519jni/scripts/jni_jar_prereq.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+# http://redsymbol.net/articles/unofficial-bash-strict-mode/
+set -euo pipefail
+IFS=$'\n\t'
+
+if ${trace:-false}
+then
+ set -x
+fi
+
+script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+ed25519jni_jvm_dir="${script_dir}/../jvm"
+ed25519jni_rust_dir="${script_dir}/../rust"
+
+# Script to run in order to compile a JAR with the Ed25519 JNI libraries from Rust.
+# Assumes SciJava's NativeLoader will be used.
+if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+ nativeDir="${ed25519jni_jvm_dir}/natives/linux_64"
+ nativeSuffix="so"
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+ nativeDir="${ed25519jni_jvm_dir}/natives/osx_64"
+ nativeSuffix="dylib"
+else
+ echo "JNI is unsupported on this OS. Exiting."
+ exit 1
+fi
+
+useDebug="0"
+while getopts ":d" opt; do
+ case $opt in
+ d)
+ useDebug="1"
+ ;;
+ esac
+done
+
+# Give priority to release directory, unless a debug flag was passed in.
+mkdir -p ${nativeDir}
+if [ ${useDebug} -eq "1" ]; then
+ mode=debug
+else
+ mode=release
+fi
+
+if [[ -d ${ed25519jni_rust_dir}/target/${mode} ]] ; then
+ cp -f ${ed25519jni_rust_dir}/target/${mode}/libed25519jni.a ${nativeDir}
+ cp -f ${ed25519jni_rust_dir}/target/${mode}/libed25519jni.${nativeSuffix} ${nativeDir}
+else
+ echo "Unable to obtain required libed25519jni ${mode} libraries. Exiting."
+ exit 1
+fi