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!
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:
$ 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));
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));