Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JNI code for ed25519-zebra #37

Merged
merged 10 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
droark marked this conversation as resolved.
Show resolved Hide resolved
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"
droark marked this conversation as resolved.
Show resolved Hide resolved
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")
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);
}
}