Загрузка необработанного открытого ключа ECDSA длиной 64 байта в Java

У меня есть открытый ключ ECDSA NIST P-256 в формате raw (r, s). Кажется, что нет простого способа загрузить его в объект, реализующий java.security.interfaces.ECPublicKey.

Каков самый чистый способ загрузить 64-байтовый открытый ключ, чтобы его можно было использовать для проверки подписей?


person user1094206    schedule 25.05.2015    source источник
comment
Каков формат вашего открытого ключа EC? Сжатый или несжатый?   -  person rakeb.mazharul    schedule 26.05.2015
comment
64 байта? Явно не сжатый. Если бы он был сжат, это было бы 33 байта.   -  person Maarten Bodewes    schedule 27.05.2015
comment
Верный. Он несжатый.   -  person user1094206    schedule 29.05.2015
comment
открытый ключ ECDSA - это точка (на соответствующей кривой), а компоненты несжатой точки - x и y, которые являются элементами нижележащего поля, то есть Fp; r и s являются компонентами подписи и элементами Zn, где n не равно p, хотя кривые NIST Fp имеют кофактор 1, поэтому n - это порядок кривой, который примерно близок к p per Hasse.   -  person dave_thompson_085    schedule 17.07.2018


Ответы (5)


Java 7 требуется для функциональности EC и Java 8 для кодировщика / декодера Base 64, никаких дополнительных библиотек - просто Java. Обратите внимание, что при распечатке открытый ключ будет отображаться в виде именованной кривой, чего не будет делать большинство других решений. Если у вас установлена ​​последняя версия среды выполнения, этот другой ответ более понятен.

Этот ответ будет трудным, если мы сделаем это с помощью ECPublicKeySpec. Так что давайте немного обманем и вместо этого используем X509EncodedKeySpec:

private static byte[] P256_HEAD = Base64.getDecoder().decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");

/**
 * Converts an uncompressed secp256r1 / P-256 public point to the EC public key it is representing.
 * @param w a 64 byte uncompressed EC point consisting of just a 256-bit X and Y
 * @return an <code>ECPublicKey</code> that the point represents 
 */
public static ECPublicKey generateP256PublicKeyFromFlatW(byte[] w) throws InvalidKeySpecException {
    byte[] encodedKey = new byte[P256_HEAD.length + w.length];
    System.arraycopy(P256_HEAD, 0, encodedKey, 0, P256_HEAD.length);
    System.arraycopy(w, 0, encodedKey, P256_HEAD.length, w.length);
    KeyFactory eckf;
    try {
        eckf = KeyFactory.getInstance("EC");
    } catch (NoSuchAlgorithmException e) {
        throw new IllegalStateException("EC key factory not present in runtime");
    }
    X509EncodedKeySpec ecpks = new X509EncodedKeySpec(encodedKey);
    return (ECPublicKey) eckf.generatePublic(ecpks);
}

Использование:

ECPublicKey key = generateP256PublicKeyFromFlatW(w);
System.out.println(key);

Идея заключается в создании временного ключа в кодировке X509, который благополучно заканчивается публичной точкой w в конце. Байты до этого содержат кодировку ASN.1 DER OID названной кривой и структурные служебные данные, заканчивающиеся байтом 04, указывающим несжатую точку. Вот пример, что к структуре внешнего вида, используя значение 1 и 2 для 32-байтовых X и Ю.

32-байтовые значения X и Y несжатых значений точек удалены для создания заголовка. Это работает только потому, что размер точки статический - ее положение в конце определяется только размером кривой.

Теперь все, что требуется от функции generateP256PublicKeyFromFlatW, - это добавить полученную общедоступную точку w в заголовок и пропустить ее через декодер, реализованный для X509EncodedKeySpec.


В приведенном выше коде используется необработанная несжатая общедоступная точка EC - всего 32 байта X и Y - без индикатора несжатой точки со значением 04. Конечно, легко поддерживать и 65-байтовые сжатые точки:

/**
 * Converts an uncompressed secp256r1 / P-256 public point to the EC public key it is representing.
 * @param w a 64 byte uncompressed EC point starting with <code>04</code>
 * @return an <code>ECPublicKey</code> that the point represents 
 */
public static ECPublicKey generateP256PublicKeyFromUncompressedW(byte[] w) throws InvalidKeySpecException {
    if (w[0] != 0x04) {
        throw new InvalidKeySpecException("w is not an uncompressed key");
    }
    return generateP256PublicKeyFromFlatW(Arrays.copyOfRange(w, 1, w.length));
}

Наконец, я сгенерировал постоянное значение заголовка P256_HEAD в базе 64, используя:

private static byte[] createHeadForNamedCurve(String name, int size)
        throws NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, IOException {
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
    ECGenParameterSpec m = new ECGenParameterSpec(name);
    kpg.initialize(m);
    KeyPair kp = kpg.generateKeyPair();
    byte[] encoded = kp.getPublic().getEncoded();
    return Arrays.copyOf(encoded, encoded.length - 2 * (size / Byte.SIZE));
}

вызывается:

String name = "NIST P-256";
int size = 256;
byte[] head = createHeadForNamedCurve(name, size);
System.out.println(Base64.getEncoder().encodeToString(head));
person Maarten Bodewes    schedule 27.05.2015

Каков самый чистый способ загрузить 64-байтовый открытый ключ, чтобы его можно было использовать для проверки подписей?

Самое чистое, что я мог собрать! Должен работать и с другими кривыми ..

ПРИМЕЧАНИЕ. Ограничено поставщиком SunJCE или Android API 26+ (могут быть другие поставщики с этой функцией, на данный момент я не знаю о них.

public static ECPublicKey rawToEncodedECPublicKey(String curveName, byte[] rawBytes) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidParameterSpecException {
    KeyFactory kf = KeyFactory.getInstance("EC");
    byte[] x = Arrays.copyOfRange(rawBytes, 0, rawBytes.length/2);
    byte[] y = Arrays.copyOfRange(rawBytes, rawBytes.length/2, rawBytes.length);
    ECPoint w = new ECPoint(new BigInteger(1,x), new BigInteger(1,y));
    return (ECPublicKey) kf.generatePublic(new ECPublicKeySpec(w, ecParameterSpecForCurve(curveName)));
}

public static ECParameterSpec ecParameterSpecForCurve(String curveName) throws NoSuchAlgorithmException, InvalidParameterSpecException {
    AlgorithmParameters params = AlgorithmParameters.getInstance("EC");
    params.init(new ECGenParameterSpec(curveName));
    return params.getParameterSpec(ECParameterSpec.class);
}
person datKiDfromNY    schedule 16.05.2019
comment
Интересный трюк для генерации параметров. Я полагаю, это также позволяет вам продолжать использовать именованные параметры, а не явные? В любом случае, определенно менее взломанный, чем мой ответ. Какие предпосылки для этого ответа? - person Maarten Bodewes; 01.07.2019
comment
Его меньше взламывают, когда он доступен ... Параметризация вашего ответа для поддержки других кривых - хорошая золотая середина. - person datKiDfromNY; 02.08.2019

Java действительно делает криптографию очень сложной.

Процедура создания открытого ключа из заданной точки EC:

  1. Постройте объект ECPoint по заданным вами координатам.
  2. Постройте объект ECParameterSpec на основе информации о вашей кривой.
  3. Создайте объект ECPublicKeySpec из вашего ECPoint и вашего ECParameterSpec объекта.
  4. Вызов KeyFactory.generatePublic() с помощью объекта ECPublicKeySpec, чтобы получить объект PublicKey.
  5. При необходимости вставьте PublicKey в ECPublicKey.

Пример ниже:

// Setup for P-256 curve params

BigInteger p256_p = new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16);

BigInteger p256_a = new BigInteger("ffffffff00000001000000000000000000000000fffffffffffffffffffffffc", 16);
BigInteger p256_b = new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
byte[] p256_seed = {
                        (byte) 0xc4, (byte) 0x9d, (byte) 0x36, (byte) 0x08, 
                        (byte) 0x86, (byte) 0xe7, (byte) 0x04, (byte) 0x93, 
                        (byte) 0x6a, (byte) 0x66, (byte) 0x78, (byte) 0xe1, 
                        (byte) 0x13, (byte) 0x9d, (byte) 0x26, (byte) 0xb7, 
                        (byte) 0x81, (byte) 0x9f, (byte) 0x7e, (byte) 0x90
                    };

BigInteger p256_xg = new BigInteger("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 16);
BigInteger p256_yg = new BigInteger("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 16);

BigInteger p256_n = new BigInteger("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16);

// Construct prime field
ECFieldFp p256_field = new ECFieldFp(p256_p);

// Construct curve from parameters
EllipticCurve p256 = new EllipticCurve(p256_field, p256_a, p256_b, p256_seed);

// Construct base point for curve
ECPoint p256_base = new ECPoint(p256_xg, p256_yg);

// Construct curve parameter specifications object
ECParameterSpec p256spec = new ECParameterSpec(p256, p256_base, p256_n, 1); // Co-factor 1 for prime curves

// ------------------------------------------------------------- //

// Construct EC point from "raw" public key
ECPoint point = new ECPoint(r, s); // r, s is of type BigInteger

// Create a EC public key specification object from point and curve
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, p256spec);

// Retrieve EC KeyFactory
KeyFactory ECFactory = KeyFactory.getInstance("EC");

// Generate public key via KeyFactory
PublicKey pubKey = ECFactory.generatePublic(pubKeySpec);
ECPublicKey ECPubKey = (ECPublicKey) pubKey;

Может быть полезно сгенерировать ECParameterSpec один раз (возможно, в блоке статического инициализатора) по соображениям производительности.

Примечание. Вероятно, существует гораздо более простой способ сгенерировать объект ECParameterSpec (например, с помощью именованных кривых), но пока я обнаружил только, что ECGenParameterSpec имеет эту функцию. Дайте мне знать в комментариях, если есть менее болезненный подход.


Чтобы избавить себя от боли, связанной с описанным выше, закодируйте свой ключ EC в X.509, который полностью описывает ключ и значительно упростит его загрузку.

В java с ECPublicKey все, что вам нужно сделать, это вызвать ECPublicKey.getEncoded() и передать / сохранить массив байтов туда, где вам понадобится следующий ключ. Затем закодированный ключ X.509 можно восстановить с помощью:

// Retrieve EC KeyFactory
KeyFactory ECFactory = KeyFactory.getInstance("EC");

// Generate public key via KeyFactory
PublicKey pubKey = ECFactory.generatePublic(new X509EncodedKeySpec(data));
ECPublicKey ECPubKey = (ECPublicKey) pubKey;

где «данные» - это закодированный массив байтов.

person initramfs    schedule 26.05.2015
comment
@MaartenBodewes Что ж, мне потребовалось больше времени, чем я ожидал, чтобы получить все константы, используемые в кривой, из двух или трех разных документов. Я все еще удивлен, что в java нет простого способа получить объект ECParameterSpec непосредственно из именованных кривых, что кажется очень полезной функцией, если она существует. - person initramfs; 27.05.2015
comment
В этом ты определенно прав. Рад поддержать запрос функции :) - person Maarten Bodewes; 27.05.2015
comment
Спасибо за ваше решение. Где ты взял константы? В частности, p256_seed? - person user1094206; 29.05.2015
comment
@ user1094206 Все они доступны в Интернете в виде общедоступных документов. Я получил большую часть информации из этого документа, но использовал некоторые подтверждающие документы для другие константы. - person initramfs; 29.05.2015

Это сработало для меня с помощью Bouncycastle:

ECParameterSpec ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
ECNamedCurveSpec params = new ECNamedCurveSpec("secp256r1", spec.getCurve(), spec.getG(), spec.getN());
ECPoint publicPoint =  ECPointUtil.decodePoint(params.getCurve(), publicKeyByteArray);
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(publicPoint, params);
PublicKey publicKey =  keyFactory.generatePublic(pubKeySpec);
person Hollerweger    schedule 16.03.2016
comment
Это зависит от BouncyCastle, который не присутствует автоматически (кроме Android в виде spongycastle). - person dave_thompson_085; 16.08.2018
comment
Да, обновил ответ. Мне показалось самым простым решением получить java.security.PublicKey - person Hollerweger; 16.08.2018

Открытый ключ EC - это точка, состоящая из координат x и y. Один раз я написал следующий сегмент кода, чтобы преобразовать точки EC x, y в объект publicKey. Надеюсь, что это поможет вам. Довожу до вашего сведения:

rawPubKey = 04 + координата x + координата y (шестнадцатеричная строка)

curveName = P-256 (строка)

Пример точки открытого ключа EC для P-256:

rawPubKey = 04 6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296 4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B3156837ECBB6

Провайдер BC: вам нужен провайдер Bouncy Castle. Я использовал bcprov-jdk15on-149.jar, но вы можете загрузить последнюю версию со страницы здесь.

/**
 * This method converts the uncompressed raw EC public key into java.security.interfaces.ECPublicKey
 * @param rawPubKey 
 * @param curveName
 * @return java.security.interfaces.ECPublicKey
 */
public ECPublicKey ucPublicKeyToPublicKey(String rawPubKey, String curveName) {
    byte[] rawPublicKey = Helper.toByte(rawPubKey); 
    ECPublicKey ecPublicKey = null;
    KeyFactory kf = null;
    
    ECNamedCurveParameterSpec ecNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec(curveName);
    ECCurve curve = ecNamedCurveParameterSpec.getCurve();
    EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, ecNamedCurveParameterSpec.getSeed());
    java.security.spec.ECPoint ecPoint = ECPointUtil.decodePoint(ellipticCurve, rawPublicKey);
    ECParameterSpec ecParameterSpec = EC5Util.convertSpec(ellipticCurve, ecNamedCurveParameterSpec);
    java.security.spec.ECPublicKeySpec publicKeySpec = new java.security.spec.ECPublicKeySpec(ecPoint, ecParameterSpec);
    
    kf = java.security.KeyFactory.getInstance("EC");
    
    try {
        ecPublicKey = (ECPublicKey) kf.generatePublic(publicKeySpec);
    } catch (Exception e) {
        System.out.println("Caught Exception public key: " + e.toString());
    }
    
    return ecPublicKey;
}

РЕДАКТИРОВАТЬ: Вот метод toByte():

public static byte[] toByte(String hex) {
        if (hex == null)
            return null;
        hex = hex.replaceAll("\\s", "");
        byte[] buffer = null;
        if (hex.length() % 2 != 0) {
            hex = "0" + hex;
        }
        int len = hex.length() / 2;
        buffer = new byte[len];
        for (int i = 0; i < len; i++) {
            buffer[i] = (byte) Integer.parseInt(
                    hex.substring(i * 2, i * 2 + 2), 16);
        }
        return buffer;
    }

Но вы можете использовать свою реализацию. Вот еще один:

import javax.xml.bind.DatatypeConverter;
public static byte[] toByte(String hex) {{
    return DatatypeConverter.parseHexBinary(hex);
}
person rakeb.mazharul    schedule 26.05.2015
comment
Пожалуйста, по крайней мере расскажите людям, какие библиотеки вы используете и какие вспомогательные функции, такие как Helper.toByte, выполняют (и зачем они вообще нужны). - person Maarten Bodewes; 27.05.2015
comment
@MaartenBodewes Я отредактировал свой ответ. Ty. Я думал, что OP может использовать свой собственный способ (вспомогательную или встроенную библиотеку) для преобразования hex string в byte array. :) - person rakeb.mazharul; 27.05.2015
comment
Хорошо, но это не очень удачно названный метод - я не мог видеть, что это шестнадцатеричный, хотя у меня были свои подозрения. ECNamedCurveTable, похоже, является определенным классом Bouncy Calstle, верно? - person Maarten Bodewes; 27.05.2015
comment
Спасибо! Я бы хотел избежать использования bouncycastle по разным причинам. - person user1094206; 29.05.2015