ECC PublicKey的Java紧凑表示

java.security.PublicKey#getEncoded()返回密钥的X509表示,在ECC的情况下,与原始ECC值相比增加了很多开销。

我希望能够在大多数紧凑的表示中将PublicKey转换为字节数组(反之亦然)(即尽可能小的字节块)。

KeyType(ECC)和具体曲线类型是预先已知的,因此不需要对它们的信息进行编码。

解决方案可以使用Java API,BouncyCastle或任何其他自定义代码/库(只要许可证并不意味着需要使用它的开源专有代码)。

这个function也出现在Bouncy Castle中,但我将展示如何使用Java来解决这个问题,以防有人需要它:

 import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.util.Arrays; public class Curvy { private static final byte UNCOMPRESSED_POINT_INDICATOR = 0x04; public static ECPublicKey fromUncompressedPoint( final byte[] uncompressedPoint, final ECParameterSpec params) throws Exception { int offset = 0; if (uncompressedPoint[offset++] != UNCOMPRESSED_POINT_INDICATOR) { throw new IllegalArgumentException( "Invalid uncompressedPoint encoding, no uncompressed point indicator"); } int keySizeBytes = (params.getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE; if (uncompressedPoint.length != 1 + 2 * keySizeBytes) { throw new IllegalArgumentException( "Invalid uncompressedPoint encoding, not the correct size"); } final BigInteger x = new BigInteger(1, Arrays.copyOfRange( uncompressedPoint, offset, offset + keySizeBytes)); offset += keySizeBytes; final BigInteger y = new BigInteger(1, Arrays.copyOfRange( uncompressedPoint, offset, offset + keySizeBytes)); final ECPoint w = new ECPoint(x, y); final ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(w, params); final KeyFactory keyFactory = KeyFactory.getInstance("EC"); return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec); } public static byte[] toUncompressedPoint(final ECPublicKey publicKey) { int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE; final byte[] uncompressedPoint = new byte[1 + 2 * keySizeBytes]; int offset = 0; uncompressedPoint[offset++] = 0x04; final byte[] x = publicKey.getW().getAffineX().toByteArray(); if (x.length <= keySizeBytes) { System.arraycopy(x, 0, uncompressedPoint, offset + keySizeBytes - x.length, x.length); } else if (x.length == keySizeBytes + 1 && x[0] == 0) { System.arraycopy(x, 1, uncompressedPoint, offset, keySizeBytes); } else { throw new IllegalStateException("x value is too large"); } offset += keySizeBytes; final byte[] y = publicKey.getW().getAffineY().toByteArray(); if (y.length <= keySizeBytes) { System.arraycopy(y, 0, uncompressedPoint, offset + keySizeBytes - y.length, y.length); } else if (y.length == keySizeBytes + 1 && y[0] == 0) { System.arraycopy(y, 1, uncompressedPoint, offset, keySizeBytes); } else { throw new IllegalStateException("y value is too large"); } return uncompressedPoint; } public static void main(final String[] args) throws Exception { // just for testing final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); kpg.initialize(163); for (int i = 0; i < 1_000; i++) { final KeyPair ecKeyPair = kpg.generateKeyPair(); final ECPublicKey ecPublicKey = (ECPublicKey) ecKeyPair.getPublic(); final ECPublicKey retrievedEcPublicKey = fromUncompressedPoint( toUncompressedPoint(ecPublicKey), ecPublicKey.getParams()); if (!Arrays.equals(retrievedEcPublicKey.getEncoded(), ecPublicKey.getEncoded())) { throw new IllegalArgumentException("Whoops"); } } } } 

试图在java中生成一个未压缩的表示几乎杀了我! 希望我早些时候能找到这个(尤其是Maarten Bodewes的优秀答案)。 我想在答案中指出一个问题并提供改进:

 if (x.length <= keySizeBytes) { System.arraycopy(x, 0, uncompressedPoint, offset + keySizeBytes - x.length, x.length); } else if (x.length == keySizeBytes + 1 && x[0] == 0) { System.arraycopy(x, 1, uncompressedPoint, offset, keySizeBytes); } else { throw new IllegalStateException("x value is too large"); } 

这个丑陋的位是必要的,因为BigInteger吐出字节数组表示的方式:“ 数组将包含表示此BigInteger所需的最小字节数,包括至少一个符号位 ”( toByteArray javadoc )。 这意味着a。)如果设置了xy的最高位,则将在数组前面添加一个0x00 ,并且b。)将调整前导0x00 。 第一个分支处理修剪的0x00 ,第二个分支处理前置的0x00

“修剪前导零”导致代码中的问题确定xy的预期长度:

 int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE; 

如果曲线的order具有前导0x00则它将被截断并且不被bitLength考虑。 生成的密钥长度太短。 获得p的比特长度的令人难以置信的(但是正确的)方法是:

 int keySizeBits = publicKey.getParams().getCurve().getField().getFieldSize(); int keySizeBytes = (keySizeBits + 7) >>> 3; 

+7用于补偿不是2的幂的位长度。)

此问题影响至少一条标准JCA( X9_62_c2tnb431r1 )提供的曲线,该曲线具有前导零的顺序:

 000340340340340 34034034034034034 034034034034034 0340340340323c313 fab50589703b5ec 68d3587fec60d161c c149c1ad4a91 

这是我用来解压公钥的BouncyCastle方法:

 public static byte[] extractData(final @NonNull PublicKey publicKey) { final SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); final byte[] encodedBytes = subjectPublicKeyInfo.getPublicKeyData().getBytes(); final byte[] publicKeyData = new byte[encodedBytes.length - 1]; System.arraycopy(encodedBytes, 1, publicKeyData, 0, encodedBytes.length - 1); return publicKeyData; } 

使用BouncyCastle, ECPoint.getEncoded(true)返回该点的压缩表示。 基本上仿射X坐标与仿射Y的符号位。