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:
parent
0e7a96a267
commit
2abe8b96b5
|
@ -1,2 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
target/
|
||||
|
|
|
@ -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).
|
|
@ -0,0 +1 @@
|
|||
/natives/
|
|
@ -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")
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
sbt.version=1.4.6
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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!"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
sbt.version=1.4.5
|
|
@ -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"]
|
|
@ -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 _
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub(crate) mod exception;
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue