Programmer
Programmer

Reputation: 150

Ed25519 in JDK 15, Parse public key from byte array and verify

Since Ed25519 has not been around for long (in JDK), there are very few resources on how to use it.

While their example is very neat and useful, I have some trouble understanding what am I doing wrong regarding key parsing.

They Public Key is being read from a packet sent by an iDevice.

(Let's just say, it's an array of bytes)

From the searching and trying my best to understand how the keys are encoded, I stumbled upon this message.

   4.  The public key A is the encoding of the point [s]B.  First,
       encode the y-coordinate (in the range 0 <= y < p) as a little-
       endian string of 32 octets.  The most significant bit of the
       final octet is always zero.  To form the encoding of the point
       [s]B, copy the least significant bit of the x coordinate to the
       most significant bit of the final octet.  The result is the
       public key.

That means that if I want to get y and isXOdd I have to do some work. (If I understood correctly)

Below is the code for it, yet the verifying still fails.

I think, I did it correctly by reversing the array to get it back into Big Endian for BigInteger to use.

My questions are:

  1. Is this the correct way to parse the public key from byte arrays
  2. If it is, what could possibly be the reason for it to fail the verifying process?

// devicePublicKey: ByteArray
val lastIndex = devicePublicKey.lastIndex
val lastByte = devicePublicKey[lastIndex]
val lastByteAsInt = lastByte.toInt()
val isXOdd = lastByteAsInt.and(255).shr(7) == 1

devicePublicKey[lastIndex] = (lastByteAsInt and 127).toByte()

val y = devicePublicKey.reversedArray().asBigInteger

val keyFactory = KeyFactory.getInstance("Ed25519")
val nameSpec = NamedParameterSpec.ED25519
val point = EdECPoint(isXOdd, y)
val keySpec = EdECPublicKeySpec(nameSpec, point)
val key = keyFactory.generatePublic(keySpec)

Signature.getInstance("Ed25519").apply {
    initVerify(key)
    update(deviceInfo)
    println(verify(deviceSignature))
}

And the data (before manipulation) (all in HEX):

Device identifier: 34444432393531392d463432322d343237442d414436302d444644393737354244443533
Device public key: e0a611c84db0ae91abfe2e6db91b6a457a4b41f9d8e09afdc7207ce3e4942e94
Device signature: a0383afb3bcbd43d08b04274a9214036f16195dc890c07a81aa06e964668955b29c5026d73d8ddefb12160529eeb66f843be4a925b804b575e6a259871259907
Device info: a86a71d42874b36e81a0acc65df0f2a84551b263b80b61d2f70929cd737176a434444432393531392d463432322d343237442d414436302d444644393737354244443533e0a611c84db0ae91abfe2e6db91b6a457a4b41f9d8e09afdc7207ce3e4942e94
// Device info is simply concatenated [hkdf, identifier, public key]

And the public key after the manipulation:

e0a611c84db0ae91abfe2e6db91b6a457a4b41f9d8e09afdc7207ce3e4942e14

Thank you very much, and every bit of help is greatly appreciated. This will help many more who will stumble upon this problem at a later point, when the Ed25519 implementation will not be so fresh.

Upvotes: 7

Views: 2782

Answers (4)

jpe7s
jpe7s

Reputation: 11

Thanks to programmer for their question/answer and derek-mccallum for their example as well. The Java interfaces for this task are not intuitive at all and their content saved me some time.

Here is a more succinct example and the code is available on GitHub here.

static java.security.PublicKey toJavaPublicKey(byte[] publicKey, int off, int len) {
  byte[] reversed = ByteUtil.reverse(publicKey, off, len);

  int last = reversed[0] & 0xFF;
  boolean xOdd = (last & 0b1000_0000) == 0b1000_0000;
  reversed[0] = (byte) (last & Byte.MAX_VALUE);
  var y = new BigInteger(reversed);

  var edECPoint = new EdECPoint(xOdd, y);
  var publicKeySpec = new EdECPublicKeySpec(NamedParameterSpec.ED25519, edECPoint);
  try {
    return ED_25519_KEY_FACTORY.generatePublic(publicKeySpec);
  } catch (InvalidKeySpecException e) {
    throw new RuntimeException(e);
  }
}

Upvotes: 1

Alex Suzuki
Alex Suzuki

Reputation: 1192

You can check how it is done in the OpenJDK implementation: https://github.com/openjdk/jdk15/blob/master/src/jdk.crypto.ec/share/classes/sun/security/ec/ed/EdDSAPublicKeyImpl.java#L65

Basically encodedPoint is your byte array (just the plain bytes, without ASN.1 encoding).

Upvotes: -2

Derek McCallum
Derek McCallum

Reputation: 39

Helped me a lot. Would never have figured it out without your example. I did it in java.

public static PublicKey getPublicKey(byte[] pk)
            throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidParameterSpecException {
        // key is already converted from hex string to a byte array.
        KeyFactory kf = KeyFactory.getInstance("Ed25519");
        // determine if x was odd.
        boolean xisodd = false;
        int lastbyteInt = pk[pk.length - 1];
        if ((lastbyteInt & 255) >> 7 == 1) {
            xisodd = true;
        }
        // make sure most significant bit will be 0 - after reversing.
        pk[pk.length - 1] &= 127;
        // apparently we must reverse the byte array...
        pk = ReverseBytes(pk);
        BigInteger y = new BigInteger(1, pk);

        NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");
        EdECPoint ep = new EdECPoint(xisodd, y);
        EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, ep);
        PublicKey pub = kf.generatePublic(pubSpec);
        return pub;
    

Upvotes: 3

Programmer
Programmer

Reputation: 150

Actually, the whole encoding and decoding is correct. The one thing in the end, that was the problem was that I (by mistake) reversed the array I read one too many times.

Reversing arrays since certain keys are encoded in little endian, while in order to represent it as a BigInteger in JVM, you have to reverse the little endian so it becomes big endian.

Hopefully this helps everyone in the future who will get stuck on any similar problems.

If there will be any questions, simply comment here or send me a message here. I'll do my best to help you out.

Upvotes: 2

Related Questions