Как подписать данные с помощью закрытого ключа ECDSA с помощью кривой P-256 (secp256k1) в Android Kotlin?

Я хотел подписать шестнадцатеричные данные, используя закрытый ключ ECDSA с кривой P-256 (secp256k1). Но большинство методов в Android используют закрытый ключ в кодировке PKCS#8 для генерации подписи. Как преобразовать закрытый ключ ECDSA с кривой P-256 (secp256k1) в PKCS#8, закодированный в Android Kotlin?

Текущий формат закрытого ключа: -----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIKUpeefDapsFwkh3nvxEtDkkh3eA......-----END EC PRIVATE KEY -----

Требуемый формат закрытого ключа: -----BEGIN PRIVATE KEY-----\nMHQCAQEEIKUpeefDapsFwkh3nvxEtDkkh3eA......-----END PRIVATE KEY -----

Я не могу использовать Bouncy Castle, поскольку targetSdkVersion моего проекта — 33. Я также попробовал следующий метод для анализа закрытого ключа и генерации подписи, но получал сообщение «java.security.spec.InvalidKeySpecException: com.android.org.conscrypt.OpenSSLX509CertificateFactory$ParsingException: ошибка анализа закрытого ключа»

fun parseECPrivateKey(pem: String): PrivateKey {
    // Remove the header and footer from the PEM string
    val privateKeyPEM = pem
        .replace("-----BEGIN EC PRIVATE KEY-----", "")
        .replace("-----END EC PRIVATE KEY-----", "")
        .replace("\\s".toRegex(), "")

    // Decode the Base64 encoded string
    val encoded = Base64.getDecoder().decode(privateKeyPEM)

    // Use KeyFactory to convert the PKCS8 encoded key into a PrivateKey object
    val keyFactory = KeyFactory.getInstance("EC")
    val keySpec = PKCS8EncodedKeySpec(encoded)
    return keyFactory.generatePrivate(keySpec)
}

fun signData(privateKey: PrivateKey, dataHex: String): ByteArray {
    val data = hexStringToByteArray(dataHex)
    val signature = Signature.getInstance("SHA256withECDSA")
    signature.initSign(privateKey)
    signature.update(data)
    return signature.sign()
}

fun hexStringToByteArray(hex: String): ByteArray {
    val len = hex.length
    val data = ByteArray(len / 2)
    for (i in 0 until len step 2) {
        data[i / 2] = ((Character.digit(hex[i], 16) shl 4)
                + Character.digit(hex[i + 1], 16)).toByte()
    }
    return data
}

fun main() {
    val privateKeyPem = """
        -----BEGIN EC PRIVATE KEY-----
        MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0m4yLz+sdzZtBG9Q
        HQ9++wcfq1O4hOWgSBMb/A6eijyhRANCAAQeB0fBl2D7HZOKVBjpPiU2jabzNxQU
        ZYrJ+MSA3LpzZxmRk2JaFHNujjkJghQT19HHjg3Fnkb8Y9oIhB9neXBI
        -----END EC PRIVATE KEY-----
    """.trimIndent()

    val dataHex = "48656c6c6f2c20576f726c6421" // Example data

    try {
        val privateKey = parseECPrivateKey(privateKeyPem)
        val hash = hexStringToByteArray(dataHex)
        val signature = signData(privateKey, hash)

        println("Signature: ${Base64.getEncoder().encodeToString(signature)}")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

Вы можете использовать инструмент (например, OpenSSL) для преобразования ключа из формата SEC1 в формат PKCS#8.

Topaco 23.05.2024 15:33

Обратите внимание, что P-256 не идентичен secp256k1. Другое название P-256 — secp256r1 или prime256v1.

Topaco 23.05.2024 15:39

спасибо @Topaco, но мне нужно реализовать преобразование внутри моего приложения для Android, поскольку закрытый ключ будет передан со стороны сервера. Использование OpenSSL непосредственно в приложении Android — сложный процесс.

Nahan R N 23.05.2024 15:56

Почему BouncyCastle нельзя использовать в качестве сторонней библиотеки для уровня API 26?

Topaco 23.05.2024 16:05

Я отредактировал уровень API в вопросе. Ссылка на причину здесь @Topaco

Nahan R N 23.05.2024 16:36

Ссылка не проясняет мне вашу проблему. Вы обеспокоены запланированным в будущем удалением устаревших функций у поставщика BC? Пожалуйста, говорите прямо и не заставляйте нас гадать. Вы можете удалить предустановленный BC Provider и импортировать любую другую версию BC. в чем именно проблема?:

Topaco 23.05.2024 17:26

Альтернативно, возьмите любой ключ PKCS#8, закодированный в DER, для P-256 (или любую другую кривую, которую вы используете) и (программно) замените необработанные секретный и открытый ключи на ключи вашего реального ключа SEC1.

Topaco 23.05.2024 17:50

@Topaco всякий раз, когда я пытаюсь использовать BC в качестве поставщика, он выдает исключение «невозможно преобразовать пару ключей: поставщик BC больше не предоставляет реализацию KeyFactory.EC. См. android-developers.googleblog.com/2018/03/» … для получения более подробной информации». . и если я попытаюсь проанализировать ключ без использования поставщика, я получу «$ParsingException: ошибка анализа закрытого ключа», как я уже упоминал в вопросе. Чтобы заменить секретный ключ из ключа SEC1, мне снова нужно сначала проанализировать мой ключ SEC1. Я новичок в этом и не смог найти другого работающего решения.

Nahan R N 23.05.2024 18:22

См. мой ответ для подробного описания импорта ключа EC в кодировке PEM в формате SEC1 с использованием BouncyCastle.

Topaco 23.05.2024 21:28
1
9
174
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Импорт ключа, закодированного в формате PEM, в формате SEC1 можно выполнить с помощью BouncyCastle. Для этого необходимо сначала указать необходимые зависимости BouncyCastle в app/gradle:

implementation("org.bouncycastle:bcpkix-jdk15to18:1.78.1")
implementation("org.bouncycastle:bcprov-jdk15to18:1.78.1")

Удалите предустановленную версию БК и импортируйте текущую:

import org.bouncycastle.jce.provider.BouncyCastleProvider
...
Security.removeProvider("BC")
Security.addProvider(BouncyCastleProvider())

Затем ключ EC в формате SEC1, закодированный в PEM, можно импортировать следующим образом:

import org.bouncycastle.openssl.PEMKeyPair
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
...
val sec1Pem =   """
                -----BEGIN EC PRIVATE KEY-----
                MHcCAQEEIK1vV4iLOPym9KvJJU5hd6CMEp+DTt8QI7NPBdJSf+VDoAoGCCqGSM49
                AwEHoUQDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNh
                YrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
                -----END EC PRIVATE KEY-----
                """.trimIndent()
val pemParser = PEMParser(StringReader(sec1Pem))
val pemKeyPair = pemParser.readObject() as PEMKeyPair
val privateKey = JcaPEMKeyConverter().getKeyPair(pemKeyPair).private

Сгенерированный таким образом privateKey можно передать непосредственно в ваш метод signData().


Редактировать:

Ключ, опубликованный в комментарии к этому ответу, представляет собой ключ в формате SEC1 для кривой secp256k1:

-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIKUpeefDapsFwkh3nvxEtDkkh3eAuP5ufcoTYIi9UxWooAcGBSuBBAAK
oUQDQgAEqHKJEruS1urTo8gED6xqGxeIiiGE0Yeapj4k0uAXuOtWn9EIhfAfo4gK
0KiKmMgG3LC3T8Ry/09KU3tLmxLzsg==
-----END EC PRIVATE KEY-----

как это легче всего увидеть, когда ключ загружен в анализатор ASN.1, например. вот .

Как уже говорилось в комментариях к вопросу, P-256 (он же secp256r1 он же prime256v1) и secp256k1 — это две разные кривые!
Импорт ключей, описанный в этом ответе, импортирует ключи в формате SEC1 для любых кривых (включая P-256 и secp256k1).
Однако это не означает, что провайдер, используемый для подписи, должен поддерживать все эти кривые.
В отличие от P-256, secp256k1, по-видимому, не поддерживается поставщиком по умолчанию, предположительно AndroidOpenSSL 1.0 (по крайней мере, на моей машине): использование secp256k1 приводит к исключению в initSign().

Эту проблему можно быстро решить, используя провайдера BouncyCastle, который поддерживает более широкий диапазон кривых и который вы все равно уже используете из-за импорта ключей.
Все, что вам нужно сделать, это добавить поставщика в качестве второго параметра при создании экземпляра подписи в методе signData():

val signature = Signature.getInstance("SHA256withECDSA", "BC")

При этом изменении используется (указанный) поставщик BC и работает подпись с помощью кривой secp256k1.


Также обратите внимание, что ключ в кодировке PEM, опубликованный в вашем вопросе, недействителен: тело содержит ключ в формате PKCS#8, а верхний/нижний колонтитул принадлежит ключу в формате SEC1.

Ключ в кодировке PEM в формате PKCS#8 с фиксированным верхним/нижним колонтитулом:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0m4yLz+sdzZtBG9Q
HQ9++wcfq1O4hOWgSBMb/A6eijyhRANCAAQeB0fBl2D7HZOKVBjpPiU2jabzNxQU
ZYrJ+MSA3LpzZxmRk2JaFHNujjkJghQT19HHjg3Fnkb8Y9oIhB9neXBI
-----END PRIVATE KEY-----

или тот же ключ в формате SEC1, закодированный PEM:

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINJuMi8/rHc2bQRvUB0PfvsHH6tTuITloEgTG/wOnoo8oAoGCCqGSM49
AwEHoUQDQgAEHgdHwZdg+x2TilQY6T4lNo2m8zcUFGWKyfjEgNy6c2cZkZNiWhRz
bo45CYIUE9fRx44NxZ5G/GPaCIQfZ3lwSA==
-----END EC PRIVATE KEY-----

Кроме того, это ключ для кривой P-256 (он же secp256r1 он же prime256v1), см. здесь.

Поскольку вы программно удаляете несогласованный верхний/нижний колонтитул и разрывы строк, а Base64 декодирует ключ (т. е. преобразует его в действительный ключ, закодированный DER в формате PKCS#8), работает импорт с помощью parseECPrivateKey() или PKCS8EncodedKeySpec, а также подпись с поставщиком по умолчанию. который поддерживает P-256 (я могу подтвердить это тестом на своей машине).
Однако, как только вы примените настоящий ключ SEC1, импорт не удастся (поскольку PKCS8EncodedKeySpec не может обрабатывать ключи SEC1, но требует ключа PKCS#8). Аналогично, как только вы используете кривую secp256k1, подписывание с поставщиком по умолчанию не выполняется.

Я уже пробовал этот пример и получал ту же ошибку, что и в вопросе «Ошибка анализа закрытого ключа». Но мне удалось сгенерировать подпись, используя закрытый ключ в вашем ответе. Пожалуйста, посмотрите закрытый ключ, который я получаю в json "-----BEGIN EC PRIVATE KEY -----\nMHQCAQEEIKUpeefDapsFwkh3nvxEtDkkh3eAuP5ufcoTYIi9Ux‌​WooAcGBSuBBAAK\noUQD‌​QgAEqHKJEruS1urTo8gE‌​D6xqG xeIiiGE0Yeapj4k‌​0uAXuOtWn9EIhfAfo4gK‌​\n0KiKmMgG3LC3T8Ry/0‌​9KU3tLmxLzsg==\n ----‌​-КОНЕЦ ЧАСТНОГО КЛЮЧА EC-----\n". Этот ключ успешно используется для генерации подписи в iOS Swift. @Топако

Nahan R N 24.05.2024 08:58

@NahanRN - Это проблема не с импортом ключа, а с самой подписью, с. раздел «Редактировать» моего ответа для получения более подробной информации.

Topaco 25.05.2024 12:19

Спасибо @Topaco за немедленное и комплексное решение. Security.removeProvider("BC") сделал всю работу за меня. Определенно, принимаю ответ!

Nahan R N 27.05.2024 17:13

Другие вопросы по теме