From 2abe8b96b5350db0cb9d0f960606fca286fa8acb Mon Sep 17 00:00:00 2001 From: Douglas Roark Date: Fri, 26 Feb 2021 14:58:38 -0800 Subject: [PATCH] Add JNI code for ed25519-zebra (#37) * Add JNI code for ed25519-zebra Add some code allowing other languages, via JNI, to interact with ed25519-zebra. The initial commit: - Allows users to obtain a random 32 byte signing key seed. - Allows users to obtain a 32 byte verification key from a signing key seed. - Allows users to sign arbitrary data. - Allows users to verify an Ed25519 signature. - Includes a Java file that can be used. - Includes some Scala-based JNI tests. * Review fixups - Minor Rust code optimizations. - Rust build optimizations. - Tweak the JNI JAR prereq script to match the new outputs. * Significant cleanup - More build system tidying. The primary goal is to try to firewall the JNI code from everything else. - README tidying. * Grab bag of improvements - Clean up the wrapper classes (streamlining, make constructors private, more mutability safety). - private -> public for a static variable intended for public usage. - Minor comment & build system cleanup. * Bump JNI version to 0.0.4-DEV Decided to bump the version to reflect earlier changes. * Hard-code the ed25519-zebra version for ed25519jni to use * Unify ed25519 JNI version Also add "-JNI" to assist with tagging and otherwise distinguish the JNI code from the main library version/code. * Add code to make VerificationKeyBytes comparison easier Also add a test suite for VerificationKeyBytes. * VerificationKeyBytes cleanup - Fix hashCode() override. - Add a test. - Remove unneecessary semicolons. * Add Signature to JNI Mirror the Signature struct from Rust and add some basic tests. Also do a bit of Scala test cleanup. --- .gitignore | 2 +- ed25519jni/README.md | 49 +++++++ ed25519jni/jvm/.gitignore | 1 + ed25519jni/jvm/build.sbt | 25 ++++ ed25519jni/jvm/project/Deps.scala | 24 ++++ ed25519jni/jvm/project/build.properties | 1 + .../org/zfnd/ed25519/Ed25519Interface.java | 136 ++++++++++++++++++ .../main/java/org/zfnd/ed25519/Signature.java | 94 ++++++++++++ .../java/org/zfnd/ed25519/SigningKeySeed.java | 76 ++++++++++ .../zfnd/ed25519/VerificationKeyBytes.java | 85 +++++++++++ .../zfnd/ed25519/Ed25519InterfaceTest.scala | 68 +++++++++ .../org/zfnd/ed25519/SignatureTest.scala | 49 +++++++ .../ed25519/VerificationKeyBytesTest.scala | 48 +++++++ ed25519jni/project/build.properties | 1 + ed25519jni/rust/Cargo.toml | 17 +++ ed25519jni/rust/src/lib.rs | 94 ++++++++++++ ed25519jni/rust/src/utils.rs | 1 + ed25519jni/rust/src/utils/exception.rs | 115 +++++++++++++++ ed25519jni/scripts/jni_jar_prereq.sh | 52 +++++++ 19 files changed, 937 insertions(+), 1 deletion(-) create mode 100644 ed25519jni/README.md create mode 100644 ed25519jni/jvm/.gitignore create mode 100644 ed25519jni/jvm/build.sbt create mode 100644 ed25519jni/jvm/project/Deps.scala create mode 100644 ed25519jni/jvm/project/build.properties create mode 100644 ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java create mode 100644 ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java create mode 100644 ed25519jni/jvm/src/main/java/org/zfnd/ed25519/SigningKeySeed.java create mode 100644 ed25519jni/jvm/src/main/java/org/zfnd/ed25519/VerificationKeyBytes.java create mode 100644 ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/Ed25519InterfaceTest.scala create mode 100644 ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/SignatureTest.scala create mode 100644 ed25519jni/jvm/src/test/scala/org/zfnd/ed25519/VerificationKeyBytesTest.scala create mode 100644 ed25519jni/project/build.properties create mode 100644 ed25519jni/rust/Cargo.toml create mode 100644 ed25519jni/rust/src/lib.rs create mode 100644 ed25519jni/rust/src/utils.rs create mode 100644 ed25519jni/rust/src/utils/exception.rs create mode 100755 ed25519jni/scripts/jni_jar_prereq.sh 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