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.
This commit is contained in:
Douglas Roark 2021-02-26 14:58:38 -08:00 committed by GitHub
parent 0e7a96a267
commit 2abe8b96b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 937 additions and 1 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
/target
Cargo.lock
target/

49
ed25519jni/README.md Normal file
View File

@ -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
<a name="jar"></a>
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).

1
ed25519jni/jvm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/natives/

25
ed25519jni/jvm/build.sbt Normal file
View File

@ -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")

View File

@ -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,
)
}

View File

@ -0,0 +1 @@
sbt.version=1.4.6

View File

@ -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);
}
}

View File

@ -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<Signature> 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;
}
}

View File

@ -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<SigningKeySeed> 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!"));
}
}

View File

@ -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<VerificationKeyBytes> 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);
}
}

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1 @@
sbt.version=1.4.5

View File

@ -0,0 +1,17 @@
[package]
name = "ed25519jni"
version = "0.0.4-JNI-DEV"
authors = ["Douglas Roark <douglas.roark@gemini.com>"]
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"]

View File

@ -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 _
}

View File

@ -0,0 +1 @@
pub(crate) mod exception;

View File

@ -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<T> = thread::Result<Result<T, Error>>;
// 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<T>(env: &JNIEnv, res: ExceptionResult<T>, 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<dyn Any + Send>) -> String {
if let Some(s) = any.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = any.downcast_ref::<String>() {
s.clone()
} else if let Some(error) = any.downcast_ref::<Box<dyn std::error::Error + Send>>() {
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<dyn Error + Send> = Box::new("e".parse::<i32>().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<T: Send + 'static>(val: T) -> Box<dyn Any + Send> {
panic::catch_unwind(panic::AssertUnwindSafe(|| panic!(val))).unwrap_err()
}
}

View File

@ -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