【问题标题】:Java compact representation of ECC PublicKeyECC PublicKey 的 Java 紧凑表示
【发布时间】:2023-11-24 03:28:01
【问题描述】:

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

我希望能够以最紧凑的表示形式(即尽可能小的字节块)将 PublicKey 转换为字节数组(反之亦然)。

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

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

【问题讨论】:

    标签: java encoding cryptography public-key elliptic-curve


    【解决方案1】:

    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");
                }
            }
        }
    }
    

    【讨论】:

    • 我使用了标准的未压缩点符号。这意味着指示符字节当然是开销。此外,还有压缩点符号,但压缩点存在一些 IP 问题,我认为 Bouncy 对点压缩有一些支持。
    • 无压缩点是可以的,有些公司有点压缩相关的专利。您已经提到功能存在于 BC 本身中,您能否添加示例以说明如何使用 BC API 来实现它?
    • @Daimon 你能自己看看吗?我目前不使用压缩点,所以基本上要自己写,而且时间紧迫。
    • 抱歉造成误会。我不是在问点压缩,而是使用 BC API 进行公钥 字节转换 :) 如果我知道该怎么做,我根本不会问主要问题
    • 好的,但是你看起来有多努力? ECPoint.getEncoded()ECPoint.Fp.getEncoded(boolean compressed) 并不是那么很难找到。
    【解决方案2】:

    这是我用来解压公钥的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;
    }
    

    【讨论】:

      【解决方案3】:

      试图在 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 所需的最小字节数,包括至少一个符号位 em>" (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
      

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

          2021 年只需使用 Tink

          public static byte[] pointEncode(EllipticCurves.CurveType curveType,
                                           EllipticCurves.PointFormatType format,
                                           ECPoint point)
                                    throws GeneralSecurityException
          

          【讨论】: