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 all commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/target
Cargo.lock
target/
49 changes: 49 additions & 0 deletions ed25519jni/README.md
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions ed25519jni/jvm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/natives/
25 changes: 25 additions & 0 deletions ed25519jni/jvm/build.sbt
Original file line number Diff line number Diff line change
@@ -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")
24 changes: 24 additions & 0 deletions ed25519jni/jvm/project/Deps.scala
Original file line number Diff line number Diff line change
@@ -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,
)
}
1 change: 1 addition & 0 deletions ed25519jni/jvm/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.4.6
136 changes: 136 additions & 0 deletions ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
94 changes: 94 additions & 0 deletions ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java
Original file line number Diff line number Diff line change
@@ -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;
}
}