В один прекрасный день наш сертификат подписи кода протух.
Ну протух и протух, случается. У нас же есть новый сертификат! Щас переподпишем, и всё заработает!
А вот и нет. У нового сертификата — новая цепочка доверия, а владельцы системы куда мы ставимся не настроены устанавливать сертификаты от (в принципе весьма известного) CA в своё хранилище доверенных сертификатов.
Но они готовы использовать на своей стороне скрипт на powershell, который будет проверять валидность, а потом устанавливать без проверки подписей. Да и мы хотим быть уверены, что устанавливаться будет именно наш код. А пакуем мы код на машине, на которую powershell ставить не хочется.
Так что призовём на помощь криптографию, и набьём немного шишек.
Пара слов о безопасности.
-
Не изобретайте собственную криптографию. Её сломают.
-
Не изобретайте странные способы использования стандартной криптографии. Это не добавит безопасности вашим программам.
-
Пользуйтесь по возможности последними стабильными версиями программ, предоставляющих криптопротоколы. Во избежание возможных проблем реализации.
Для самых нетерпеливых, финальное решение
Генерация ключей, подпись и проверка openssl:
openssl genrsa -out private.pem 4096 openssl rsa -in private.pem -pubout -out public.pem sha512sum artifact.zip | cut -f 1 -d\ | xxd -r -p | \ openssl dgst -sign ./private.pem \ -sigopt rsa_padding_mode:pkcs1 -sha512 > signature sha512sum artifact.zip | cut -f 1 -d\ | xxd -r -p | \ openssl dgst -verify ./public.pem -signature signature -sigopt rsa_padding_mode:pkcs1 -sha512
Загрузка ключей, подпись и проверка в PowerShell:
$hash=Get-FileHash -Path artifact.zip -Algorithm SHA512 $bytehash=[byte[]]($hash.Hash -replace '..','0x$&,' -split ',' -ne '') $rsa = [System.Security.Cryptography.RSA]::Create() $len=0 $prifile = Get-Content -Path ./private.pem $prikey = $prifile -Split '`n' | Select-String -Pattern "---" -NotMatch | Join-String $prider = [System.Convert]::FromBase64String($prikey) $rsa.ImportRSAPrivateKey($prider, [ref] $len) $sig = $rsa.SignData($bytehash, [System.Security.Cryptography.HashAlgorithmName]::SHA512, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) $pubfile = Get-Content -Path ./public.pem $pubkey = $pubfile -Split '`n' | Select-String -Pattern "---" -NotMatch | Join-String $pubder = [System.Convert]::FromBase64String($pubkey) $rsa.ImportSubjectPublicKeyInfo($pubder, [ref] $len) $rsa.VerifyData($bytehash, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA512, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
Делаем подпись
Очевидным решением будет подписать распространяемый артефакт и раздавать подпись с артефактом. В качестве базового возьмём решение на основе openssl и RSA — подписываем приватным ключом, проверяем публичным.
Специально для нас в openssl есть блок операций «низкого» уровня — pkeyutl
# создать ключи, приватный и публичный openssl genrsa -out private.pem 4096 openssl rsa -in private.pem -pubout -out public.pem # подписать файл openssl pkeyutl -sign -inkey ./private.pem -in artifact.zip #> Public Key operation error #> 140052316043152:error:0406C06E:rsa routines:RSA_padding_add_PKCS1_type_1:data too large for key size:rsa_pk1.c:75:
Оопс! Не работает…
RSA не позволяет подписывать сообщения, которые больше чем размер ключа минус обязательный заголовок.
Ну да ладно, не будем переживать. У нас есть проверенный способ — захешируем файл. Ключ у нас большой, так что возьмём SHA512:
# Попытка 1 # подписываем выхлоп sha512sum sha512sum artifact.zip | openssl pkeyutl -sign -inkey ./private.pem > signature # и тут же его проверяем sha512sum artifact.zip | \ openssl pkeyutl -verify -pubin -sigfile ./signature -inkey ./public.pem #> Signature Verified Successfully
Красота! А вот если у нас вдруг имя файла поменялось?
sha512sum artifact-15052022.zip | \ openssl pkeyutl -verify -pubin -sigfile ./signature -inkey ./public.pem #> Signature Verification Failure
Оопс! Не работает…
Забыли, что sha512sum выдаёт имя файла после дайджеста. Да и как-то некрасиво подписывать шестнадцатеричный дамп. Попробуем сделать бинарный блоб из шестнадцатеричного дампа с помощью xxd:
# Попытка 2 # переведём хеш в бинарный блоб и подпишем sha512sum artifact.zip | cut -f 1 -d\ | xxd -r -p | \ openssl pkeyutl -sign -inkey ./private.pem > signature # проверим на файле с новым именем sha512sum artifact-15052022.zip | cut -f 1 -d\ | xxd -r -p | \ openssl pkeyutl -verify -pubin -sigfile ./signature -inkey ./public.pem #> Signature Verified Successfully
Подпись есть. Но как ей пользоваться?
Powershell вступает в игру
Специально для нас у Микрософта в дотнете есть класс System.Security.Cryptography.RSA.
Попробуем загрузить сгенерированные ключи:
$rsa = [System.Security.Cryptography.RSA]::Create() $len=0 $prifile = Get-Content -AsByteStream -Path ./private.pem $prifile = [byte[]]$prifile $rsa.ImportRSAPublicKey($prifile, [ref] $len) #> MethodInvocationException: Exception calling "ImportRSAPublicKey" with "2" argument(s): "error:0B09407D:x509 certificate routines:x509_pubkey_decode:public key decode error"
Оопс! Не работает.
У нас ключ в формате pem (то есть base64 дамп ключа с текстовой разбивкой), а класс ожидает ключ в формате der (бинарный блоб). Исправляем:
$prifile = Get-Content -Path ./private.pem $prikey = $prifile -Split '`n' | Select-String -Pattern "---" -NotMatch | Join-String $prider = [System.Convert]::FromBase64String($prikey) $rsa.ImportRSAPrivateKey($prider, [ref] $len)
Ура, работает! А если попробовать подписать?
Сигнатура метода подписи такая:
public byte[] SignData ( byte[] data, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding);
Не так быстро! А при чём здесь хеш? А, мы же уже выяснили, что можем подписать только сообщение ограниченной длины.
Тогда возьмём System.Security.Cryptography.HashAlgorithmName.SHA512 (как удачно мы выбрали sha512sum!) и, для определённости, System.Security.Cryptography.RSASignaturePadding.Pkcs1:
$data=[byte[]] (Get-Content -AsByteStream -Path ./artifact.zip) # Оопс, вроде бы пара мегабайт, а тупит знатно. Ну ладно, начнём с этого. $sig = $rsa.SignData($data, [System.Security.Cryptography.HashAlgorithmName]::SHA512, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) $sig[0] #> 140
Оопс! Не совсем то. Первый байт подписи созданной через openssl — 0x10. Но к этому мы ещё вернёмся чуть позже. Попробуем загрузить публичный ключ и проверить нативную подпись:
$pubfile = Get-Content -Path ./public.pem $pubkey = $pubfile -Split '`n' | Select-String -Pattern "---" -NotMatch | Join-String $pubder = [System.Convert]::FromBase64String($pubkey) $rsa.ImportRSAPublicKey($pubder, [ref] $len) #> MethodInvocationException: Exception calling "ImportRSAPublicKey" with "2" argument(s): "error:0B09407D:x509 certificate routines:x509_pubkey_decode:public key decode error"
Да что ж тебе на этот раз не понравилось?!
Впрочем, у класса RSA внезапно есть второй метод загрузки публичного ключа:
$rsa.ImportSubjectPublicKeyInfo($pubder, [ref] $len) # Ну ок, посмотрим, что там с подписью... $rsa.VerifyData($data, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA512, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) #> True
Ок, по отдельности с powershell и openssl разобрались, будем их дружить.
Shaken, not stirred…
Во-первых, поправим вызов openssl чтобы сгенерированные подписи совпали — укажем использованные алгоритмы хеширования и паддинга
sha512sum artifact.zip | cut -f 1 -d\ | xxd -r -p | \ openssl pkeyutl -sign -inkey ./private.pem \ -pkeyopt rsa_padding_mode:pkcs1 -pkeyopt digest:sha512 > signature hexdump -n 1 signature #> 0000000 008c
Так гораздо лучше! Полученную таким образом подпись можно использовать в powershell`е пользуясь методом выше.
Во-вторых — почему-то у меня powershell грузил пару мегабайт входного файла десяток секунд. Это не дело. Размер боевого артефакта может быть и гигабайт, и больше. Сделаем просто и незамысловато: будем подписывать не sha512 от файла, а sha512 от sha512 от файла:
sha512sum artifact.zip | cut -f 1 -d\ | xxd -r -p | \ sha512sum | cut -f 1 -d\ | xxd -r -p | \ openssl pkeyutl -sign -inkey ./private.pem \ -pkeyopt rsa_padding_mode:pkcs1 \ -pkeyopt digest:sha512 > signature
$hash=Get-FileHash -Path artifact.zip -Algorithm SHA512 $bytehash=[byte[]]($hash.Hash -replace '..','0x$&,' -split ',' -ne '') $rsa.VerifyData($bytehash, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA512, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) #> True
В третьих — legacy. Куда без него!
Всё вышеописанное прекрасно работает на системе с последним openssl, но внезапно на системе сборки очень старый openssl, и нет команды pkeyutl как класса. Есть команда rsautl, но в её параметрах нельзя задать тип хеша и паддинга. А ещё есть команда dgst…
Путём научного тыка выясняется, что можно было не городить огород с sha512sum. Ну почти — с учетом необходимости подписывать хеш от хеша.
openssl dgst -sign ./private.pem \ -sigopt rsa_padding_mode:pkcs1 -sha512 artifact.zip > signature hexdump -n 1 ./signature #> 0000000 008c openssl dgst -verify ./public.pem \ -sigopt rsa_padding_mode:pkcs1 -sha512 -signature signature artifact.zip #> Verified OK
Чтобы было совсем хорошо, в той версии openssl которая стоит на системе сборки ещё нет параметра -sigopt, но, к нашему счастью, там по дефолту применяется Pkcs1.
В таком виде решение и публичный ключ отправляются клиентам на согласование, а мы ищем куда бы заныкать приватный ключ, так, чтобы не потерять и не засветить.
Ссылки:
https://ru.wikipedia.org/wiki/RSA
https://www.openssl.org/docs/manmaster/man1/openssl.html
https://www.openssl.org/docs/man1.1.1/man1/openssl-pkeyutl.html
https://www.openssl.org/docs/man1.1.1/man1/dgst.html
https://man7.org/linux/man-pages/man1/sha512sum.1.html
https://linux.die.net/man/1/xxd
https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsa?view=net-6.0
https://ru.wikipedia.org/wiki/X.509#Общеупотребительные_расширения_файлов_сертификатов
https://medium.com/@bn121rajesh/rsa-sign-and-verify-using-openssl-behind-the-scene-bf3cac0aade2
ссылка на оригинал статьи https://habr.com/ru/post/665768/
Добавить комментарий