Skip to content

Commit

Permalink
Add JNI code for ed25519-zebra
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Douglas Roark committed Jan 27, 2021
1 parent 014d823 commit 137c57b
Show file tree
Hide file tree
Showing 20 changed files with 702 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
Cargo.lock
natives/
target/
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,25 @@ ed25519-zebra-legacy = { package = "ed25519-zebra", version = "1" }
ed25519-zebra-zip215 = { package = "ed25519-zebra", version = "2" }
```

## Example
## JNI

Code that provides a [JNI](https://en.wikipedia.org/wiki/Java_Native_Interface) for the library is included. Tests written in Scala have also been included. In order to run the tests, follow the [JAR deployment method](#jar) listed below.

To build the JNI code, run `cargo build` in the `ed25519jni` subdirectory. The generated Rust libraries can then be used, alongside the included Java interface file, in two different manners.

### Direct library usage

Use a preferred method to load the Rust libraries directly and include the Java interface file in your project.

### 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 steps involved.

- Once the Rust code has been compiled, run `jni_jar_prereq.sh` from the root directory.
- Run `sbt publishLocal` from the root directory. (`sbt packageBin` can be used instead to build and place the JAR in `ed25519jni/target` but the resultant JAR won't be auto-loaded via NativeLoader.)

## Library Usage Example

```
use std::convert::TryFrom;
Expand Down
14 changes: 14 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
lazy val root = project
.in(file("."))
.aggregate(
ed25519jni
)

lazy val ed25519jni = project
.in(file("ed25519jni"))
.settings(
unmanagedResourceDirectories in Compile += baseDirectory.value / "natives"
)
.enablePlugins()

publishArtifact in root := false
2 changes: 2 additions & 0 deletions ed25519jni/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target-dir = "target/rust"
17 changes: 17 additions & 0 deletions ed25519jni/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "ed25519jni"
version = "0.0.1"
authors = ["Douglas Roark <douglas.roark@gemini.com>"]
license = "MIT OR Apache-2.0"
publish = false
edition = "2018"

[dependencies]
ed25519-zebra = { path = "../" }
failure = "0.1.8"
jni = "0.18.0"

[lib]
name = "ed25519jni"
path = "src/main/rust/lib.rs"
crate-type = ["staticlib", "cdylib"]
16 changes: 16 additions & 0 deletions ed25519jni/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ed25519_zebra JNI code
Code that allows for any JNI-using language to interact with specific `ed25519-zebra` calls.

## Compilation
`cargo build`

## Testing
`sbt test`

## Usage
The code must be able to access the `libed25519jni` dynamic library / shared object generated by Rust. Once compiled and loaded, code can use the accompanying Java file to access specific `ed25519-zebra`. The calls include but are not necessarily limited to:

* Generating a random 32 byte signing key seed.
* Generating a 32 byte verification key from a signing key seed.
* Signing arbitrary data with a signing key.
* Verifying a signature for arbitrary data with a verification key.
13 changes: 13 additions & 0 deletions ed25519jni/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name := "ed25519jni"

version := "0.0.1"

autoScalaLibrary := false

crossPaths := false

libraryDependencies ++= Deps.ed25519jni

publishArtifact := true

testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
19 changes: 19 additions & 0 deletions ed25519jni/project/Deps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import sbt._

object Deps {

object V {
val nativeLoaderV = "2.3.4"
val scalaTest = "3.0.5"
}

object Test {
val nativeLoader = "org.scijava" % "native-lib-loader" % V.nativeLoaderV
val scalaTest = "org.scalatest" %% "scalatest" % V.scalaTest % "test"
}

val coreTest = List(
Test.nativeLoader,
Test.scalaTest,
)
}
1 change: 1 addition & 0 deletions ed25519jni/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.4.5
122 changes: 122 additions & 0 deletions ed25519jni/src/main/java/org/zfnd/ed25519/Ed25519Interface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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 {
public static final int SEED_LEN = 32;

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[SEED_LEN];
rng.nextBytes(seedBytes);
BigInteger sb = new BigInteger(seedBytes);
while(sb == BigInteger.ZERO) {
rng.nextBytes(seedBytes);
sb = new BigInteger(seedBytes);
}

return seedBytes;
}

// Public frontend to use when generating a signing key seed.
//
// @return sksb Java object containing an EdDSA signing key seed
// @throws RuntimeException if ???
public static SigningKeySeed genSigningKeySeed(SecureRandom rng) {
return new SigningKeySeed(genSigningKeySeedFromJava(rng));
}

// Check if verification key bytes for a verification key are valid.
//
// @return true if valid, false if not
// @param vk_bytes 32 byte verification key bytes to verify
// @throws RuntimeException if ???
public static native boolean checkVerificationKeyBytes(byte[] vk_bytes);

// Get verification key bytes from a signing key seed.
//
// @return vkb 32 byte verification key
// @param sk_seed_bytes 32 byte signing key seed
// @throws RuntimeException if ???
private static native byte[] getVerificationKeyBytes(byte[] sk_seed_bytes);

// Get verification key bytes from a signing key seed.
//
// @return vkb VerificationKeyBytes object
// @param sk_seed_bytes SigningKeySeed object
// @throws RuntimeException if ???
public static VerificationKeyBytes getVerificationKeyBytes(SigningKeySeed sksb) {
return new VerificationKeyBytes(getVerificationKeyBytes(sksb.getSigningKeySeed()));
}

// Creates a signature on msg using the given signing key.
//
// @return sig 64 byte signature
// @param sk_seed_bytes 32 byte signing key seed
// @param msg Message of arbitrary length to be signed
// @throws RuntimeException if ???
private static native byte[] sign(byte[] sk_seed_bytes, byte[] msg);

// Creates a signature on msg using the given signing key.
//
// @return sig 64 byte signature
// @param sk_seed_bytes 32 byte signing key seed
// @param msg Message of arbitrary length to be signed
// @throws RuntimeException if ???
public static byte[] sign(SigningKeySeed sksb, byte[] msg) {
return sign(sksb.getSigningKeySeed(), msg);
}

/// Verifies a purported `signature` on the given `msg`.
//
// @return true if verified, false if not
// @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
// @throws RuntimeException if ???
private static native boolean verify(byte[] vk_bytes, byte[] sig, byte[] msg);

/// Verifies a purported `signature` on the given `msg`.
//
// @return true if verified, false if not
// @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
// @throws RuntimeException if ???
public static boolean verify(VerificationKeyBytes vkb, byte[] sig, byte[] msg) {
return verify(vkb.getVerificationKeyBytes(), sig, msg);
}
}
62 changes: 62 additions & 0 deletions ed25519jni/src/main/java/org/zfnd/ed25519/SigningKeySeed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 SEED_LENGTH = 32;
private static final Logger logger = LoggerFactory.getLogger(SigningKeySeed.class);
private byte[] seed = new byte[SEED_LENGTH];

// Determining if bytes are valid is pretty trivial. Rust code not needed.
private static boolean bytesAreValid(final byte[] seedBytes) {
boolean valid = false;
if(seedBytes.length == SEED_LENGTH) {
for (int b = 0; b < SEED_LENGTH; b++) {
if (seedBytes[b] != 0) {
valid = true;
break;
}
}
}

return valid;
}

public SigningKeySeed(final byte[] seedBytes) {
if(bytesAreValid(seedBytes)) {
seed = Arrays.copyOf(seedBytes, SEED_LENGTH);
}
else {
throw new IllegalArgumentException("Attempted to create invalid signing "
+ "key seed - Bytes were invalid");
}
}

public byte[] getSigningKeySeed() {
return seed;
}

public static Optional<SigningKeySeed> fromBytes(final byte[] seedBytes) {
Optional<SigningKeySeed> sks = Optional.empty();

try {
sks = Optional.of(new SigningKeySeed(seedBytes));
}
catch (IllegalArgumentException e) {
logger.error("Attempted to create invalid signing key seed - Illegal "
+ "argument exception has been caught and ignored");
}
finally {
return sks;
}
}

public static SigningKeySeed fromBytesOrThrow(final byte[] seedBytes) {
// The constructor already throws, so this method can YOLO the creation.
return new SigningKeySeed(seedBytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 {
private static final int BYTES_LENGTH = 32;
private static final Logger logger = LoggerFactory.getLogger(VerificationKeyBytes.class);
private byte[] vkb = new byte[BYTES_LENGTH];

// Determining if bytes are valid is complicated. Call into Rust.
private static boolean bytesAreValid(final byte[] verificationKeyBytes) {
if(verificationKeyBytes.length == BYTES_LENGTH) {
return Ed25519Interface.checkVerificationKeyBytes(verificationKeyBytes);
}
else {
return false;
}
}

public VerificationKeyBytes(final byte[] verificationKeyBytes) {
if(bytesAreValid(verificationKeyBytes)) {
vkb = Arrays.copyOf(verificationKeyBytes, BYTES_LENGTH);
}
else {
throw new IllegalArgumentException("Attempted to create invalid "
+ "verification key bytes (input not valid)");
}
}

public byte[] getVerificationKeyBytes() {
return vkb;
}

public static Optional<VerificationKeyBytes> fromBytes(final byte[] verificationKeyBytes) {
Optional<VerificationKeyBytes> vkb = Optional.empty();

try {
vkb = Optional.of(new VerificationKeyBytes(verificationKeyBytes));
}
catch (IllegalArgumentException e) {
logger.error("Attempted to create invalid verification key bytes - Illegal "
+ "argument exception has been caught and ignored");
}
finally {
return vkb;
}
}

public static VerificationKeyBytes fromBytesOrThrow(final byte[] verificationKeyBytes) {
// The constructor already throws, so this method can YOLO the creation.
return new VerificationKeyBytes(verificationKeyBytes);
}
}
Loading

0 comments on commit 137c57b

Please sign in to comment.