В один прекрасный день наш сертификат подписи кода протух.
Ну протух и протух, случается. У нас же есть новый сертификат! Щас переподпишем, и всё заработает!
А вот и нет. У нового сертификата - новая цепочка доверия, а владельцы системы куда мы ставимся не настроены устанавливать сертификаты от (в принципе весьма известного) 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 не позволяет подписывать сообщения, которые больше чем размер ключа минус обязательный заголовок, 40 бит, что ли?
Ну да ладно, не будем переживать. У нас есть проверенный способ - захешируем файл. Ключ у нас большой, так что возьмём 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#Общеупотребительные_расширения_файлов_сертификатов