Android Java Spongycastle ECDSA Подпись к thin.crypto Javascript

Я импортирую набор значений со своего веб-сайта, написанного на Javascript, с использованием thin.crypto для подписи сообщений. В QR-коде я помещаю значения X, Y и D ключа из Javascript, это мой код для репликации ключа:

public static KeyPair GenerateExistingKeyPair(String d, String x, String y) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
    Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
    Log.d(TAG, "GenerateExistingKeyPair: PrivateKey D: " + d);
    Log.d(TAG, "GenerateExistingKeyPair: PublicKey X: " + x);
    Log.d(TAG, "GenerateExistingKeyPair: PublicKey Y: " + y);

    BigInteger privateD = decode(d);
    BigInteger publicX = decode(x);
    BigInteger publicY = decode(y);

    KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);;
    ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("P-256");
    ECPoint Q = ecSpec.getG().multiply(privateD);

    ECPrivateKeySpec privSpec = new ECPrivateKeySpec(privateD, ecSpec);
    ECPublicKeySpec pubSpec = new ECPublicKeySpec(Q, ecSpec);

    PrivateKey privKey = keyFactory.generatePrivate(privSpec);
    PublicKey pubKey = keyFactory.generatePublic(pubSpec);

    KeyPair keyPair = new KeyPair(pubKey, privKey);
    Log.d(TAG, "GenerateExistingKeyPair: KeyPair: " + keyPair.getPrivate().toString());
    Log.d(TAG, "GenerateExistingKeyPair: " + Hex.toHexString(privKey.getEncoded()));
    return keyPair;

}

«Декодирование», которое я использую, потому что эти значения хранятся в Base64 в Javascript.

public static BigInteger decode(String value) {
    byte[] decoded = android.util.Base64.decode(value, android.util.Base64.URL_SAFE);
    BigInteger bigInteger = new BigInteger(Hex.toHexString(decoded), 16);
    return bigInteger;
}

Теперь вот вывод из этого.

    D/ECDSA:: GenerateExistingKeyPair: PrivateKey D: m-lI_bV8YoNgAgNGpccXPdNtRJ4I6k0hdMdKD7NDYlI
          GenerateExistingKeyPair: PublicKey X: BadCycqeFycXoL4ONkATL7vu1ZxlF66JmrSgbE2A4eY
          GenerateExistingKeyPair: PublicKey Y: obTA6W6xluIdXcqRjnvq0Nh-_IfiWKV4FWziJFxXHUo
D/ECDSA:: GenerateExistingKeyPair: KeyPair: EC Private Key [ed:66:72:8b:8c:1d:97:b9:82:0b:11:c8:1f:6e:db:aa:0e:bd:67:43]
                      X: 5a742c9ca9e172717a0be0e3640132fbbeed59c6517ae899ab4a06c4d80e1e6
                      Y: a1b4c0e96eb196e21d5dca918e7bead0d87efc87e258a578156ce2245c571d4a

Насколько я могу судить, X и Y верны, их обратное преобразование с использованием Base64 дает мне то же значение, что и полученное. Теперь я перехожу к части, чтобы хешировать сообщение и отправлять транзакцию через JSON с использованием WebRTC.

public static byte[] signTransaction(Wallet wallet, byte[] msgHash) throws Exception {

    Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
    Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
    ecdsaSign.initSign(wallet.getKeyPair().getPrivate());
    ecdsaSign.update(msgHash);
    byte[] signature = ecdsaSign.sign();
    Log.d(TAG, "signTransaction: " + new BigInteger(1, signature).toString(16));

    return signature;

}

Это подпись, которую я получаю:

3045022026728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155022100ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f

После небольшого исследования я обнаружил, что подписи ECDSA в Java кодируются ANS1 DER, а подписи в javascript используют формат P1363, который представляет собой просто R и S подписи.

Итак, после некоторых исследований я узнал, как извлечь эти значения из подписи.

public static BigInteger extractR(byte[] signature) throws Exception {
    int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
    int lengthR = signature[startR + 1];
    return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR));
}

public static BigInteger extractS(byte[] signature) throws Exception {
    int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
    int lengthR = signature[startR + 1];
    int startS = startR + 2 + lengthR;
    int lengthS = signature[startS + 1];
    return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS));
}

Что дало мне следующие значения:

26728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155
ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f

В последней попытке я попытался объединить эти две строки и отправить их на сторону Javascript, но он не смог проверить, эти два значения рядом имеют тот же размер в символах, что и подписи, сгенерированные в Javascript, но метод

await window.crypto.subtle.verify({name: "ECDSA", hash: {name: "SHA-256"},}, publicKey, signature, data)

В javascript по-прежнему возвращает false.

Мой вопрос: как я могу сделать подписи совместимыми между Java и Javascript? Могу ли я преобразовать его из ASN1 DER в P1363 в Javascript? Или я могу преобразовать наоборот в Java?

Любая помощь будет оценена...


person Ricardo Doberstein    schedule 22.08.2018    source источник
comment
Без кода JavaScript, который окружает verify, трудно сказать, что пошло не так, я сделал обоснованное предположение, учитывая имя вашего параметра и код Java, но это не MCVE.   -  person Maarten Bodewes    schedule 23.08.2018
comment
Обратите внимание, что ваши процедуры преобразования из DER в плоскую подпись не являются надежными. Нет необходимости создавать открытый ключ, если у вас уже есть x и y, а преобразование из байтов в строку, а затем в BigInteger — это слишком много; просто используйте new BigInteger(1, bytes).   -  person Maarten Bodewes    schedule 23.08.2018


Ответы (2)


Вы выполняете хеширование дважды, когда выполняете:

ecdsaSign.update(msgHash);

в то время как объект ecdsaSign уже выполняет хеширование SHA-256.

person Maarten Bodewes    schedule 23.08.2018
comment
Привет, Маартен, есть идеи, как извлечь значение хеширования из ecdsaSign? Я собираюсь обновить пост завтра, добавив больше кода со стороны javascript, так что, возможно, это поможет! Я действительно хэшировал с помощью SHA-256, а затем отправлял хешированные байты подписывающей стороне. Должен ли я отправлять грубые байты сообщения для подписи, а затем выполнять SHA256, чтобы получить его значение? - person Ricardo Doberstein; 24.08.2018
comment
Да, если вам нужен хэш сообщения, то это то, что вы должны сделать! - person Maarten Bodewes; 24.08.2018

Отвечая на свой вопрос, просто чтобы закрыть его, я нашел причину, по которой я не аутентифицировался на стороне JS. Все мои методы создания и репликации открытых/закрытых ключей работают. Мой код для преобразования подписи ASN1 DER в значение R||S также работает.

Я подписывал транзакции поверх хеш-строки, как и думал, что это делал браузер. Оказывается, браузер не подписывал необработанную хеш-строку, но он сделал это с хэш-строкой перед подписью:

async sign(msg) {
    const encoder = new TextEncoder('utf-8');
    const msgBuffer = encoder.encode(msg.toString());
    const signedBuffer = await ECDSA.sign(this.keys.privateKey, msgBuffer);
    const signedArray = Array.from(new Uint8Array(signedBuffer));
    return Encryption.byteToHexString(signedArray);
}

обратите внимание на строки:

Как оказалось, браузер кодировал хэш-строку в UTF-8 и подписывал этот массив байтов размером 64, а не строку, которая имела бы 20 или около того байтов. Итак, раньше, когда браузер пытался проверить мою подпись, он фактически делал то же самое с моей хеш-строкой, преобразованной в UTF-8, и поэтому моя подпись терпела неудачу, потому что я не подписывал то же сообщение, которое пытался проверить браузер. .

Если бы я сделал это погружение в бухту JS более тщательно, это могло бы сэкономить мне около 2 дней.

Спасибо Maarten Bodewes за попытку помочь мне, вы действительно указали на несколько недостатков в моем коде и извините за отсутствие кода на стороне JS, который я вам представил, вы, вероятно, могли заметить эту проблему и помогли мне 2 дня назад.

person Ricardo Doberstein    schedule 24.08.2018