Вычисление подписи ECDSA в Java в соответствии с тестовым вектором RFC

Я пишу тестовую обвязку на java для программы, относящейся к протоколу ikev2. В рамках этого мне нужно иметь возможность рассчитать подпись ECDSA (в частности, используя кривую NIST P-256).

RFC 4754 Описывает использование ECDSA в IKEv2 и предоставляет набор тестовых векторов (включая для кривой p256, которая мне нужна).

Я пытаюсь запустить значения тестового вектора ECDSA-256 (Раздел 8.1 в RFC) через реализацию подписи ECDSA в java, используя следующий код:

//"abc" for the input
byte[] input = { 0x61, 0x62, 0x63 };

//Ugly way of getting the ECParameterSpec for the P-256 curve by name as opposed to specifying all the parameters manually.
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec kpgparams = new ECGenParameterSpec("secp256r1");
kpg.initialize(kpgparams);
ECParameterSpec params = ((ECPublicKey) kpg.generateKeyPair().getPublic()).getParams();

//Create the static private key W from the Test Vector
ECPrivateKeySpec static_privates = new ECPrivateKeySpec(new BigInteger("DC51D3866A15BACDE33D96F992FCA99DA7E6EF0934E7097559C27F1614C88A7F", 16), params);
KeyFactory kf = KeyFactory.getInstance("EC");
ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(static_privates);

//Perfrom ECDSA signature of the data with SHA-256 as the hash algorithm
Signature dsa = Signature.getInstance("SHA256withECDSA");
dsa.initSign(spriv);
dsa.update(input);

byte[] output = dsa.sign();
System.out.println("Result: " + new BigInteger(1, output).toString(16));

Результат должен быть:

CB28E099 9B9C7715 FD0A80D8 E47A7707 9716CBBF 917DD72E 97566EA1 C066957C 86FA3BB4 E26CAD5B F90B7F81 899256CE 7594BB1E A0C89212 748BFF3B 3D5B0315

Вместо этого я получаю:

30460221 00dd9131 edeb5efd c5e718df c8a7ab2d 5532b85b 7d4c012a e5a4e90c 3b824ab5 d7022100 9a8a2b12 9e10a2ff 7066ff79 89aa73d5 ba37e36858 7ec21c858 5ec

Я знаю, что разница в длине связана с кодировкой подписи Java ASN.1. Однако в остальном это совершенно неправильно, и я не понимаю, почему.

Любая помощь или совет будут очень признательны!

P.S. Я не являюсь экспертом по криптографии ECDSA или Java, поэтому, вероятно, это глупая ошибка, которую я совершаю.


person PinkyNoBrain    schedule 11.11.2013    source источник


Ответы (2)


Я предполагаю, что каждый раз, когда вы запускаете свою программу, вы получаете другое значение подписи для одного и того же открытого текста (подлежащего подписи).

ECDSA указывает, что для каждой подписи генерируется случайный эфемерный закрытый ключ ECDSA. С этой целью Signature.getInstance("SHA256withECDSA") не позволяет вам указать эфемерный ключ (это хорошо, чтобы предотвратить множество выстрелов себе в ногу!). Вместо этого он получает собственный экземпляр SecureRandom, который сделает ваши выходные данные недетерминированными.

Вероятно, это означает, что вы не можете использовать JCE (Signature.getInstance()) для проверки тестового вектора.

Что вы могли бы сделать, так это расширить SecureRandom таким образом, чтобы он возвращал детерминированные данные. Очевидно, вы не должны использовать это в реальном развертывании:

public class FixedSecureRandom extends SecureRandom {
    private static boolean debug = false;
    private static final long serialVersionUID = 1L;
    public FixedSecureRandom() { }
    private int nextBytesIndex = 0;

    private byte[] nextBytesValues = null;

    public void setBytes(byte[] values) {
        this.nextBytesValues = values; 
    }

    public void nextBytes(byte[] b) {
        if (nextBytesValues==null) { 
            super.nextBytes(b);
        } else if (nextBytesValues.length==0) { 
            super.nextBytes(b);
        } else {
            for (int i=0; i<b.length; i++) {
                b[i] = nextBytesValues[nextBytesIndex];
                nextBytesIndex = (nextBytesIndex + 1) % nextBytesValues.length;
            }
        }
    }
}

Фу. Хорошо, теперь у вас есть класс SecureRandom, который возвращает вам некоторое количество известных байтов, а затем возвращается к настоящему SecureRandom после этого. Еще раз скажу (извините за крик) - НЕ ИСПОЛЬЗУЙТЕ ЭТО В ПРОДАКШН!

Далее вам нужно будет использовать реализацию ECDSA, которая позволяет указать собственный SecureRandom. Для этой цели вы можете использовать ECDSASigner BouncyCastle. За исключением того, что здесь вы собираетесь дать ему свой собственный пиратский FixedSecureRandom, так что, когда он вызывает secureRandom.getBytes(), он получает нужные вам байты. Это позволяет вам управлять эфемерным ключом, чтобы он соответствовал указанному в тестовых векторах. Возможно, вам придется изменить фактические байты (например, добавить нулевое предварительное заполнение), чтобы они соответствовали тому, что ECDSASigner собирается запросить.

ECPrivateKeyParameters ecPriv = ...; // this is the user's EC private key (not ephemeral)

FixedSecureRandom fsr_k = new FixedSecureRandom();
fsr_k.setBytes(tempKeyK);

ECDSASigner signer = new ECDSASigner();
ParametersWithRandom ecdsaprivrand = new ParametersWithRandom(ecPriv, fsr_k);
signer.init(true, ecdsaprivrand);

Обратите внимание, что BC ECDSASigner реализует только часть подписи EC, а не хеширование. Вам все равно нужно будет сделать собственное хеширование (при условии, что ваши входные данные находятся в data):

Digest md = new SHA256Digest()
md.reset();
md.update(data, 0, data.length);
byte[] hash = new byte[md.getDigestSize()];
md.doFinal(hash, 0);

перед созданием подписи ECDSA:

BigInteger[] sig = signer.generateSignature(hash);

Наконец, эти BigInteger[] (должна быть длина==2) являются значениями (r,s). Вам потребуется ASN.1 DER-кодировать его, что должно дать вам байты droids, которые вы ищете.

person tsechin    schedule 11.11.2013
comment
Успех! Спасибо за быстрый и очень подробный ответ. Как только я прочитал это, это обрело смысл, и теперь все работает для меня. Огромное спасибо за помощь :-) - person PinkyNoBrain; 11.11.2013
comment
P.S. Для тех, кто еще смотрит. Сначала я сделал ошибку новичка, выполнив fsr_k.setBytes(new BigInteger(9E56F509196784D963D1C0A401510EE7ADA3DCC5DEE04B154BF61AF1D5A6DECE, 16).toByteArray()) и чуть не заплакал. При этом к массиву байтов добавляется дополнительный 0 байт, и поэтому вы все равно получаете неправильное значение K :-( - person PinkyNoBrain; 11.11.2013
comment
@PinkyNoBrain Вам нужна функция под названием I2OS (целое число в строку октетов). Этот метод принимает два входных данных: целое число и длину (ключа). Он возвращает кодировку целого числа без знака с обратным порядком байтов, с 00 байтами, предшествующими размеру ключа. Теперь у вас есть неподписанная часть, убедитесь, что у вас есть и часть заполнения... - person Maarten Bodewes; 14.11.2013
comment
Огромное спасибо за: ECDSASigner реализует только часть подписи EC, а не хеширование. - person Rafał Spryszyński; 23.09.2016

вот мой полный тест, следующий за решением tsechin с использованием BouncyCastle, но придерживающийся старого доброго JCA API:

    byte[] input = { 0x61, 0x62, 0x63 };

    //Create the static private key W from the Test Vector
    ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
    org.bouncycastle.jce.spec.ECPrivateKeySpec privateKeySpec = new org.bouncycastle.jce.spec.ECPrivateKeySpec(new BigInteger("DC51D3866A15BACDE33D96F992FCA99DA7E6EF0934E7097559C27F1614C88A7F", 16), parameterSpec);
    KeyFactory kf = KeyFactory.getInstance("EC");
    ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(privateKeySpec);

    //Perfrom ECDSA signature of the data with SHA-256 as the hash algorithm
    Signature dsa = Signature.getInstance("SHA256withECDSA", "BC");
    FixedSecureRandom random = new FixedSecureRandom();
    random.setBytes(Hex.decode("9E56F509196784D963D1C0A401510EE7ADA3DCC5DEE04B154BF61AF1D5A6DECE"));
    dsa.initSign(spriv, random);
    dsa.update(input);
    byte[] output = dsa.sign();

    // compare the signature with the expected reference values
    ASN1Sequence sequence = ASN1Sequence.getInstance(output);
    DERInteger r = (DERInteger) sequence.getObjectAt(0);
    DERInteger s = (DERInteger) sequence.getObjectAt(1);
    Assert.assertEquals(r.getValue(), new BigInteger("CB28E0999B9C7715FD0A80D8E47A77079716CBBF917DD72E97566EA1C066957C", 16));
    Assert.assertEquals(s.getValue(), new BigInteger("86FA3BB4E26CAD5BF90B7F81899256CE7594BB1EA0C89212748BFF3B3D5B0315", 16));
person bjoern    schedule 11.11.2013
comment
Помоги мне. При выполнении ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(privateKeySpec); приложение игнорирует остальную часть кода - person nani; 29.09.2016