Search code examples
javapythonkey

Read EC Public Key, works in python, error in java


I have this issue moving some code from python to java. The following public key:

MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC2X07fCab+nIPAWBb5eRlhdOfR0Bkrhx7TgM3cGbR31g=

can be succesfully read from this python code:

key = bytearray.fromhex(
   "3039301306072a8648ce3d020106082a8648ce3d030107032200"
) + bytearray([0x03]) + bytearray.fromhex("d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58")
device_public_key = load_der_public_key(
                key
            )

printing out device_public_key I see:

<cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey object at 0x102372cd0>

When trying to do the same in Java, I fail in every possibile try:

    fun loadKey() {
       
        val key  = Hex.decode("3039301306072a8648ce3d020106082a8648ce3d03010703220003d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58")
        try {
            read(key)
        } catch (e: Exception) {
            println("Failed to load for sign $sign: $e")
        }
    }

    private fun read(publicKey: ByteArray) {
        val spec: ECNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1")
        val kf = KeyFactory.getInstance("ECDSA", BouncyCastleProvider())
        val params = ECNamedCurveSpec("prime256v1", spec.curve, spec.g, spec.n)
        val point: ECPoint = ECPointUtil.decodePoint(params.curve, publicKey)
        val pubKeySpec = ECPublicKeySpec(point, params)
        val t =  kf.generatePublic(pubKeySpec) as ECPublicKey
        println(t)
    }

The exception i get is:

java.lang.IllegalArgumentException: Invalid point encoding 0x30

I can't undestand what's wrong since the key is correct (works in python). I tryed to Byte64 encode/decode but it always fail.

This post is linked to my previous question (next step): https://security.stackexchange.com/questions/272048/parsing-and-loading-ec-private-key-curve-secp256r1

The public key is different, the one i'm trying to load is defined as a concatenation of a fixed header, a signbyte and the variable I receive:

Compressed ephemeral public key used for ECDH w/ reader private key, 32 byte X coordinate. Note: when uncompressing, the Y coordinate is always even

EDIT: Thanks to @dave_thompson_085 for the explanation! Indeed the code is not Java but Kotlin (JVM compatible), I forgot to mention it (code runs on Android). My solution is similar to 2nd option, but the curve is obtained from the private key, and the compressed key (as explained) is only the sign byte (0x02) plus the body (as I understood, the body is the xCoordinate of the point).

fun load_compressed_public_key(privateKey: ECPrivateKey, compressedKey: ByteArray): ECPublicKey {
            val decodePoint = org.bouncycastle.jce.ECPointUtil.decodePoint(privateKey.params.curve, compressedKey)
            val spec = ECNamedCurveTable.getParameterSpec("secp256r1")
            val kf = java.security.KeyFactory.getInstance("ECDSA", BouncyCastleProvider())
            val params = ECNamedCurveSpec("secp256r1", spec.curve, spec.g, spec.n)
            val pubKeySpec = java.security.spec.ECPublicKeySpec(decodePoint, params)
            val uncompressed = kf.generatePublic(pubKeySpec) as ECPublicKey
            return uncompressed
        }

I also tried to use approach 1 (was my first attempt) but I failed just because I did not add BouncyCastleProvider when obtaining the KeyFactory! I did a quick test, with "BC" works, without fails with "Invalid EC Key" Below the working test (in kotlin):

    @Test
    fun `test load ephemeral x509`(){
        val spki: ByteArray = Hex.decode(
            "3039301306072a8648ce3d020106082a8648ce3d030107032200"
                    + "02d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58"
        )
        val kf1: KeyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider())
        val pub1: PublicKey = kf1.generatePublic(X509EncodedKeySpec(spki))
        println(pub1)
    }

Thanks again for the help and for the explanation, I appreciate it instead of raw code!


Solution

  • That format is not a point and trying to use or decode it as a point won't work. Also, the first byte of an X9-compressed point, what you call 'signbyte', for a point with y-coordinate even, is 0x02 not 0x03. And the language you posted isn't Java, although it is apparently calling Java.

    There are two approaches:

    1. use the format you've built, which is actually the ASN.1 structure SubjectPublicKeyInfo defined in X.509/PKIX supplemented for ECC by RFC5480:
    $ xxd -r -p <<END | openssl asn1parse -inform der -i -dump
    3039301306072a8648ce3d020106082a8648ce3d030107032200
    02d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58
    END
        0:d=0  hl=2 l=  57 cons: SEQUENCE
        2:d=1  hl=2 l=  19 cons:  SEQUENCE
        4:d=2  hl=2 l=   7 prim:   OBJECT            :id-ecPublicKey
       13:d=2  hl=2 l=   8 prim:   OBJECT            :prime256v1
       23:d=1  hl=2 l=  34 prim:  BIT STRING
          0000 - 00 02 d9 7d 3b 7c 26 9b-fa 72 0f 01 60 5b e5 e4   ...};|&..r..`[..
          0010 - 65 85 d3 9f 47 40 64 ae-1c 7b 4e 03 37 70 66 d1   e...G@d..{N.7pf.
          0020 - df 58                                             .X
    # observe that this contains an AlgorithmIdentifier for (X9-style) ECC with the desired curve
    # and a BIT STRING _containing_ (after the first byte 0x00 which is actually part of 
    # the ASN.1 encoding but OpenSSL displays as part of the value) the point 0x02 + 32 bytes
    

    This is the format used by (standard) Java crypto for publickey encoding, as well as by OpenSSL and things using OpenSSL like pyca and many others, and (thus) can be processed as-is by a suitable JCA KeyFactory:

            byte[] spki = unhex("3039301306072a8648ce3d020106082a8648ce3d030107032200"
                + "02d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58");
            KeyFactory kf1 = KeyFactory.getInstance("EC","BC");
            PublicKey pub1 = kf1.generatePublic(new X509EncodedKeySpec(spki));
    
    1. use the approach you posted, but pass it ONLY the point NOT the whole structure (i.e. WITHOUT the 'fixed prefix'). Also you don't need to convert the spec:
    import org.bouncycastle.jce.ECNamedCurveTable;
    import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
    import org.bouncycastle.jce.spec.ECPublicKeySpec;
    import org.bouncycastle.math.ec.ECPoint; // NOT the JCA one!
    ...
            byte[] raw = unhex("02d97d3b7c269bfa720f01605be5e46585d39f474064ae1c7b4e03377066d1df58");
            ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec ("prime256v1");
            ECPoint point = ecSpec.getCurve().decodePoint (raw);
            KeyFactory kf2 = KeyFactory.getInstance("EC", "BC");
            PublicKey pub2 = kf2.generatePublic(new ECPublicKeySpec(point, ecSpec));