Я хотел подписать шестнадцатеричные данные, используя закрытый ключ 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()
}
}
Обратите внимание, что P-256 не идентичен secp256k1. Другое название P-256 — secp256r1 или prime256v1.
спасибо @Topaco, но мне нужно реализовать преобразование внутри моего приложения для Android, поскольку закрытый ключ будет передан со стороны сервера. Использование OpenSSL непосредственно в приложении Android — сложный процесс.
Почему BouncyCastle нельзя использовать в качестве сторонней библиотеки для уровня API 26?
Я отредактировал уровень API в вопросе. Ссылка на причину здесь @Topaco
Ссылка не проясняет мне вашу проблему. Вы обеспокоены запланированным в будущем удалением устаревших функций у поставщика BC? Пожалуйста, говорите прямо и не заставляйте нас гадать. Вы можете удалить предустановленный BC Provider и импортировать любую другую версию BC. в чем именно проблема?:
Альтернативно, возьмите любой ключ PKCS#8, закодированный в DER, для P-256 (или любую другую кривую, которую вы используете) и (программно) замените необработанные секретный и открытый ключи на ключи вашего реального ключа SEC1.
@Topaco всякий раз, когда я пытаюсь использовать BC в качестве поставщика, он выдает исключение «невозможно преобразовать пару ключей: поставщик BC больше не предоставляет реализацию KeyFactory.EC. См. android-developers.googleblog.com/2018/03/» … для получения более подробной информации». . и если я попытаюсь проанализировать ключ без использования поставщика, я получу «$ParsingException: ошибка анализа закрытого ключа», как я уже упоминал в вопросе. Чтобы заменить секретный ключ из ключа SEC1, мне снова нужно сначала проанализировать мой ключ SEC1. Я новичок в этом и не смог найти другого работающего решения.
См. мой ответ для подробного описания импорта ключа EC в кодировке PEM в формате SEC1 с использованием BouncyCastle.
Импорт ключа, закодированного в формате 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 -----\nMHQCAQEEIKUpeefDapsFwkh3nvxEtDkkh3eAuP5ufcoTYIi9UxWooAcGBSuBBAAK\noUQDQgAEqHKJEruS1urTo8gED6xqG xeIiiGE0Yeapj4k0uAXuOtWn9EIhfAfo4gK\n0KiKmMgG3LC3T8Ry/09KU3tLmxLzsg==\n -----КОНЕЦ ЧАСТНОГО КЛЮЧА EC-----\n". Этот ключ успешно используется для генерации подписи в iOS Swift. @Топако
@NahanRN - Это проблема не с импортом ключа, а с самой подписью, с. раздел «Редактировать» моего ответа для получения более подробной информации.
Спасибо @Topaco за немедленное и комплексное решение. Security.removeProvider("BC") сделал всю работу за меня. Определенно, принимаю ответ!
Вы можете использовать инструмент (например, OpenSSL) для преобразования ключа из формата SEC1 в формат PKCS#8.