Skip to content

Commit

Permalink
Add DER & PEM support for SigningKeySeed and VerificationKeyBytes (RF…
Browse files Browse the repository at this point in the history
…C 8410) (#46)

* Add DER & PEM support for SigningKeySeed and VerificationKeyBytes (RFC 8410)

- Add encoding to and decoding from DER bytes and PEM strings for SigningKeySeed and VerificationKeyBytes.
- Add some functions so that the Java code mirrors, to a certain degree, the JDK 15 interface for Ed25519 keys and signatures.
- Add encoding and decoding for signatures (technically identity functions).
- Miscellaneous cleanup.

* Accommodate extra octet string in private key DER bytes

- In RFC 8410, DER-encoded private keys are in an octet string that's encapsulated by another octet string. Add the extra octet string, and adjust tests as necessary.
- In the tests, use the private key from RFC 8410, Sect. 10.3.

* Update pkcs8 to 0.7.0

* Cleanup

- Enhance PEM capabilities for SigningKey and VerificationKeyBytes. This also allowed for some tests to be simplified.
- From -> TryFrom for some VerificationKeyBytes impls.

* Upgrade JNI Rust bindings to PKCS8 0.7.5

- Make necessary changes to support the newer crate.
- Fix an unrelated compiler warning.

* More fixups

- Get code to compile after updating to the latest Rust.
- Fix a couple of failing tests (add LF to expected encoding output).

* Major update

- Update pkcs8 crate to 0.10.0, and update code as required to support the crate. This includes supporting the Decode(Public/Private)Key and Encode(Public/Private)Key traits so as to take advantage of Ed25519 DER and PEM code in the crate.
- Add the latest ed25519 crate (2.2.0) to support KeypairBytes and other features.
- Remove the signature code and implement Signature (Signer and Verifier traits) from the "signatures" crate included with the pkcs8 crate.
- Update the JNI code. This includes mandating Scala 3 usage.
- Minor cleanup (including warning fixes) and changes to make the code a bit clearer.

A follow-up commit will clean up the tests and probably add support for v2 private DER keys.

* Further code cleanup

- Update pkcs8 crate to 0.10.1.
- Fix PEM feature code.
- Update Ed25519 JNI code as needed.
- Remove dead code.
- Re-enable a couple of unit tests.

Note that a couple of Ed25519 JNI unit tests are still failing. A follow-up PR will have the fix.

* Add missing DER/PEM files for unit tests

* Add JNI comments to resolve publisher warnings

When executing `sbt publishLocal` and generating a JAR file, there are warnings regarding some functions not having public comments. Add public comments as needed.

* JNI README update

* Comment touchup

* Review fixups

- Finish adding PEM/PKCS8 tags and cfg items as needed to separate the features from default compilation.
- Revert some minor name changes.
- Make the JNI README more precise with regards to requirements.
- Add ARM64 macOS support to JNI. Untested but it should work, and it doesn't break Intel Macs.
- Miscellaneous cleanup, including fixing cargo and sbt warnings.

* Upgrade jni crate to 0.20.0

The 0.21.X crates feature a major refactor that breaks the code. Don't upgrade to them until some issues are resolved. (See jni-rs/jni-rs#432 for more info.)

* Upgrade jni crate to 0.21.1

- A path forward to upgrading to 0.21.X was suggested by the jni-rs library developer (jni-rs/jni-rs#439 (comment)). Upgrade the code, improving the safety of the JNI code.
- Cargo.toml fixups.

* cargo clippy / cargo fmt cleanup

Also do minor JNI README cleanup.

* Use an export to clean up some tests a bit

---------

Co-authored-by: Douglas Roark <douglas.roark@gemini.com>
  • Loading branch information
droark and Douglas Roark committed Apr 21, 2023
1 parent 7908590 commit 346f4cd
Show file tree
Hide file tree
Showing 37 changed files with 1,173 additions and 173 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ features = ["nightly"]
[dependencies]
# "digest" is exempt from SemVer, so we should always use a specific version
curve25519-dalek = { version = "=4.0.0-pre.5", default-features = false, features = ["alloc", "digest"] }
der = { version = "0.7.1", optional = true }
ed25519 = { version = "2.2.0", features = ["alloc", "pem"] }
hashbrown = "0.12.0"
hex = { version = "0.4", default-features = false, features = ["alloc"] }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
pkcs8 = { version = "0.10.1", optional = true, features = ["alloc", "pem"] }
rand_core = "0.6"
serde = { version = "1", optional = true, features = ["derive"] }
sha2 = { version = "0.10", default-features = false }
Expand All @@ -34,6 +37,8 @@ once_cell = "1.4"
[features]
nightly = []
default = ["serde", "std"]
pem = ["der"]
pkcs8 = ["dep:pkcs8"]
std = []

[[test]]
Expand Down
44 changes: 34 additions & 10 deletions ed25519jni/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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.

Note that Scala 3 is required to build and test the JNI code.

## Build Requirements
- For PEM support, the `pem` feature must be enabled in both `Cargo.toml` files
(`pem = ["der"]`).
- For PKCS #8 (DER) support, the `pkcs8` feature must be enabled in both `Cargo.toml`
files (`pkcs8 = ["dep:pkcs8"]`).

## 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
Expand All @@ -21,29 +29,45 @@ From here, there are two deployment methods: Direct library usage and JARs.

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
along with the Java JNI interface file. There are two extra 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 `jni_jar_prereq.sh` from the `ed25519jni/scripts` subdirectory. Use the `-d`
flag if working with debug builds. This script performs some JAR setup steps, and
enables the local Scala tests against the JNI code.
- Run `sbt clean publishLocal` from the `ed25519jni/jvm` subdirectory. This
generates the final `ed25519jni.jar` file.
generates the final `ed25519jni.jar` file in the {$HOME}/.ivy2/local subdirectory.

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

Note that the code is designed to support only the aforementioned JAR method. Local
changes may be required to support other deployment methods.

## 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.
### Prerequisites
To reiterate, before running any tests, execute the first step from the
[JAR compilation method](#jar) section above.

### Commands
The precise test command will depend on if you built the code with PKCS8 and/or
PEM support. Both are disabled by default. This also means that, without disabling
the associated tests or enabling the features, an `UnsatisfiedLinkError` Java error
will occur.

The following examples show how to run tests.

- `sbt test` - Run all tests. Must compile with PEM and PKCS #8 support.
- `sbt "testOnly * -- -l Pkcs8Test"` - Run all tests unrelated to PKCS #8.
- `sbt "testOnly * -- -l \"PemTest Pkcs8Test\""` - Run all tests unrelated to PEM
and PKCS #8.

## 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).
* Verify a signature for arbitrary data with verification key bytes.
* Generate DER bytes and PEM strings for signing key seeds and verification key bytes, and read back the DER bytes and PEM strings into signing key seeds and verification key bytes.
12 changes: 6 additions & 6 deletions ed25519jni/jvm/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ organization := "org.zfnd"

name := "ed25519jni"

version := "0.0.4-JNI-DEV"
version := "0.0.5-JNI-DEV"

scalaVersion := "2.12.10"
scalaVersion := "3.1.3"

scalacOptions ++= Seq("-Xmax-classfile-name", "140")
//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"
Compile / unmanagedResourceDirectories += baseDirectory.value / "natives"

publishArtifact := true

javacOptions in (Compile,doc) ++= Seq(
Compile / doc / javacOptions ++= Seq(
"-windowtitle", "JNI bindings for ed25519-zebra"
)

testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
Test / testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
7 changes: 5 additions & 2 deletions ed25519jni/jvm/project/Deps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import sbt._
object Deps {

object V {
val nativeLoaderV = "2.3.4"
val scalaTest = "3.0.9"
val nativeLoaderV = "2.4.0"
val scalactic = "3.2.15"
val scalaTest = "3.2.15"
val slf4j = "1.7.30"
}

object Test {
val nativeLoader = "org.scijava" % "native-lib-loader" % V.nativeLoaderV
val scalactic = "org.scalactic" %% "scalactic" % V.scalactic
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.scalactic,
Test.scalaTest,
Test.slf4jApi,
Test.slf4jSimple,
Expand Down
2 changes: 1 addition & 1 deletion ed25519jni/jvm/project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.4.6
sbt.version=1.8.2
123 changes: 122 additions & 1 deletion ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Ed25519Interface.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Interface class offering Java users access to certain ed25519-zebra functionality.
* Uses include but are not necessarily limited to:
* - Generating a signing key seed (essentially a private key).
* - Obtaining a DER-encoded (v1, per RFC 5958) signing key seed byte array.
* - Obtaining a PEM-encoded (v1, per RFC 5958) signing key seed string.
* - Getting verification key bytes (basically a public key) from a signing key seed.
* - Obtaining a DER-encoded (v1, per RFC 5958) verification key byte structure.
* - Obtaining a PEM-encoded (v1, per RFC 5958) verification key string.
* - Signing data with a signing key seed.
* - Verifying a signature with verification key bytes.
*/
public class Ed25519Interface {
private static final Logger logger;
private static final boolean enabled;
Expand All @@ -23,6 +35,11 @@ public class Ed25519Interface {
enabled = isEnabled;
}

/**
* Default constuctor.
*/
public Ed25519Interface() { }

/**
* Helper method to determine whether the Ed25519 Rust backend is loaded and
* available.
Expand All @@ -39,7 +56,7 @@ public static boolean isEnabled() {
* values are allowed, this code will have to stay in sync.
*
* @param rng An initialized, secure RNG
* @return sks 32 byte signing key seed
* @return the signing key seed bytes (32 bytes)
*/
private static byte[] genSigningKeySeedFromJava(SecureRandom rng) {
byte[] seedBytes = new byte[SigningKeySeed.BYTE_LENGTH];
Expand All @@ -61,6 +78,58 @@ public static SigningKeySeed genSigningKeySeed(SecureRandom rng) {
return new SigningKeySeed(genSigningKeySeedFromJava(rng));
}

/**
* Get the encoded DER (RFC 8410) bytes for signing key seed bytes.
*
* @param sks the signing key seed bytes
* @return the encoded DER bytes
*/
public static native byte[] getSigningKeySeedEncoded(byte[] sks);

/**
* Get the encoded DER (RFC 8410) bytes for signing key seed bytes.
*
* @param sks the signing key seed
* @return the encoded DER bytes
*/
public static byte[] getSigningKeySeedEncoded(SigningKeySeed sks) {
return getSigningKeySeedEncoded(sks.getSigningKeySeedCopy());
}

/**
* Get the encoded PEM (RFC 8410) string for signing key seed bytes.
*
* @param sks the signing key seed bytes
* @return the encoded PEM string
*/
public static native String getSigningKeySeedPEM(byte[] sks);

/**
* Get the encoded PEM (RFC 8410) string for signing key seed bytes.
*
* @param sks the signing key seed
* @return the encoded PEM string
*/
public static String getSigningKeySeedPEM(SigningKeySeed sks) {
return getSigningKeySeedPEM(sks.getSigningKeySeedCopy());
}

/**
* Generate a SigningKeySeed object from DER (RFC 8410) bytes.
*
* @param derBytes the encoded DER bytes (48 bytes)
* @return a new SigningKeySeed object
*/
public static native byte[] generatePrivate(byte[] derBytes);

/**
* Generate a SigningKeySeed object from a PEM (RFC 8410) string.
*
* @param pemString the encoded PEM string
* @return a new SigningKeySeed object
*/
public static native byte[] generatePrivatePEM(String pemString);

/**
* Check if verification key bytes for a verification key are valid.
*
Expand Down Expand Up @@ -88,6 +157,58 @@ public static VerificationKeyBytes getVerificationKeyBytes(SigningKeySeed seed)
return new VerificationKeyBytes(getVerificationKeyBytes(seed.getSigningKeySeed()));
}

/**
* Get the encoded DER (RFC 8410) bytes for verification key bytes.
*
* @param vkb the verification key byte array
* @return the encoded DER bytes (44 bytes)
*/
public static native byte[] getVerificationKeyBytesEncoded(byte[] vkb);

/**
* Get the encoded DER (RFC 8410) bytes for verification key bytes.
*
* @param vkb the verification key bytes
* @return the encoded DER bytes (44 bytes)
*/
public static byte[] getVerificationKeyBytesEncoded(VerificationKeyBytes vkb) {
return getVerificationKeyBytesEncoded(vkb.getVerificationKeyBytes());
}

/**
* Get the encoded PEM (RFC 8410) bytes for verification key bytes.
*
* @param vkb the verification key bytes
* @return the encoded PEM bytes
*/
public static native String getVerificationKeyBytesPEM(byte[] vkb);

/**
* Get the encoded PEM (RFC 8410) bytes for verification key bytes.
*
* @param vkb the verification key bytes
* @return the encoded PEM string
*/
public static String getVerificationKeyBytesPEM(VerificationKeyBytes vkb) {
return getVerificationKeyBytesPEM(vkb.getVerificationKeyBytes());
}

/**
* Generate a VerificationKeyBytes object from DER (RFC 8410) bytes.
*
* @param derBytes the encoded DER bytes (44 bytes)
* @return a new VerificationKeyBytes object
*/
public static native byte[] generatePublic(byte[] derBytes);

/**
* Generate a VerificationKeyBytes object from PEM (RFC 8410) bytes.
*
* @param pemString the encoded PEM string
* @return a new VerificationKeyBytes object
*/
public static native byte[] generatePublicPEM(String pemString);

/**
* Creates a signature on msg using the given signing key.
*
Expand Down
49 changes: 48 additions & 1 deletion ed25519jni/jvm/src/main/java/org/zfnd/ed25519/Signature.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@

/**
* Java wrapper class for signatures that performs some sanity checking.
*/
**/
public class Signature {
/**
* Length of signature components (R &amp; s).
**/
public static final int COMPONENT_LENGTH = 32;

/**
* Length of a signature.
**/
public static final int SIGNATURE_LENGTH = 2 * COMPONENT_LENGTH;

private static final Logger logger = LoggerFactory.getLogger(Signature.class);

private byte[] rBytes;
Expand All @@ -35,16 +43,55 @@ static boolean bytesAreValid(final byte[] signature) {
}

/**
* Get the raw signature bytes.
*
* @return a copy of the complete signature
*/
public byte[] getSignatureBytesCopy() {
return completeSignature.clone();
}

/**
* Get the raw signature bytes.
*
* @return a copy of the complete signature
*/
byte[] getSignatureBytes() {
return completeSignature;
}

/**
* Get the signature algorithm name.
*
* @return the signature algorithm name
*/
public String getAlgorithm() {
return "Ed25519";
}

/**
* Get the encoded signature bytes.
*
* @return the complete signature
*/
public byte[] getEncoded() {
return getSignatureBytes();
}

/**
* Get a Signature object from an encoded signature.
*
* @param encodedBytes untrusted, unvalidated bytes that may be an encoding of a verification key
* @return the Signature object
*/
public static Signature getFromEncoded(byte[] encodedBytes) {
if (encodedBytes.length != SIGNATURE_LENGTH) {
throw new IllegalArgumentException("Ed25519 signature must be " + SIGNATURE_LENGTH + " bytes large.");
}

return new Signature(encodedBytes);
}

/**
* Optionally convert bytes into a verification key wrapper.
*
Expand Down
Loading

0 comments on commit 346f4cd

Please sign in to comment.