Как подружить openssl и powershell, или подпись на основе RSA для самых маленьких

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В один прекрасный день наш сертификат подписи кода протух.
Ну протух и протух, случается. У нас же есть новый сертификат! Щас переподпишем, и всё заработает!
А вот и нет. У нового сертификата - новая цепочка доверия, а владельцы системы куда мы ставимся не настроены устанавливать сертификаты от (в принципе весьма известного) CA в своё хранилище доверенных сертификатов.
Но они готовы использовать на своей стороне скрипт на powershell, который будет проверять валидность, а потом устанавливать без проверки подписей. Да и мы хотим быть уверены, что устанавливаться будет именно наш код. А пакуем мы код на машине, на которую powershell ставить не хочется.
Так что призовём на помощь криптографию, и набъём немного шишек.


Пара слов о безопасности.
  1. Не изобретайте собственную криптографию. Её сломают.

  2. Не изобретайте странные способы использования стандартной криптографии. Это не добавит безопасности вашим программам.

  3. Пользуйтесь по возможности последними стабильными версиями программ, предоставляющих криптопротоколы. Во избежание возможных проблем реализации.

Для самых нетерпеливых, финальное решение

Генерация ключей, подпись и проверка 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#Общеупотребительные_расширения_файлов_сертификатов

Источник: https://habr.com/ru/post/665768/


Интересные статьи

Интересные статьи

Начинаю публикацию решений машин с площадки HackTheBox. Надеюсь этот туториал станет небольшой подсказкой тем, кто только начинает свой путь в этичном взломе. Начнем....
Мы часто проверяем большие проекты, потому что в них проще найти ошибки. А что же PVS-Studio сможет найти в небольшом проекте? Мы взяли Blend2D – библиотеку для векторной 2D-графики – и проверили свои...
IELTS — это один из двух самых популярных экзаменов для оценки уровня английского языка. Каждый год его проходит свыше 3 млн людей во всем мире. Формат экзамена впол...
Доброго времени суток, друзья! В этой статье объясняются некоторые концепции из теории музыки, на основе которых работает Web Audio API (WAA). Зная эти концепции, вы сможете принимать взве...
Всем привет. Недавно задался вопросом: «А что, если… ?». В результате родилась вот такая тема-идея: Проектирование на основе «отражения». Этой самой идеей я и хочу поделиться с Вами в данной п...