Об опасности использования eval в bash-скриптах.

Преамбула
В недавнем примере я допустил, что называется, «детский мат», грубую и далеко неочевидную новичку ошибку, о которой, впрочем, все знают. Но прописные истины неплохо повторять, так что не стоит кидаться на эту заметку с криками «боян!!!111пыщ». Для кого-то может и боян, а кому-то это может быть неизвестно. Или вот я, зная в теории об ошибке, все равно ее допустил, и даже не сразу понял, что я так грубо наебался, и мой скрипт стал небезопасным от слова «совсем».
Исходная задача
Имелся следующий набор данных в виде форматированного текста (таблицы в текстовом файле): verb666,Misha Verbitsky,+415314499922,42,11:00-16:00 ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00 dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00 vfurry1,Veniamin Furman,+415314499900,99,12:20-19:25 tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00 Исходная задача состояла в том, чтобы раскидать вывод awk в переменные bash, так, чтоб значение из соответствующей колонки таблицы попало в указанную переменную bash, и изначальный код был таким:
# ... цикл по строкам
for TMPSTRING in $(cat "testfile.txt")
do
    eval $(echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" dq $1 dq;
    print "FULLNAME=" dq $2 dq; print "PHONE=" dq $3 dq; print"ROOM=" dq $4 dq;
    print "WORKTIME=" dq $5 dq}' dq='"')
# ... делаем что-то с данными в переменных
done
# ...
Т.е. тут мы сначала получаем строчку текстового файла, передаем ее awk, awk, в свою очередь, возвращает строки: ... LOGIN="vfurry1" FULLNAME="Veniamin Furman" PHONE="+415314499922" ROOM="99" WORKTIME="12:20-19:25" ... Похоже на команду присваивания значений переменным. Да так оно и есть! Далее, полученная строка передается команде eval.
Опасность eval
В чем опасность eval? Да в том, что эта команда преобразует любую переданную ей строку в команду или команды bash, и немедленно их выполняет. Любую, абсолютно любую строку. И тут возникает ошибка безопасности. Мы получаем данные из внешнего источника. Это может быть файл с внешнего носителя, или скачанный из Интернета. В нем в худшем случае, который всегда надо держать в голове, работая с внешними источниками данных, может содержаться все, что угодно. И злоумышленнику не надо получать доступ к скрипту, достаточно изменить входные данные: verb666,Misha Verbitsky,+415314499922,42,11:00-16:00 ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00 dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00 vfurry1,Veniamin Furman `rm -rf ./testdir`,+415314499900,99,12:20-19:25 tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00 Тут, после Veniamin Furman вписана команда удаления директории ./testdir в текущем каталоге, а могло быть вписано и $HOME или даже /. Строки, взятые в двойные кавычки, интерпретируются bash’ем не только, как строки с чисто тестовыми данными. В двойных кавычках можно и нужно сделать подстановку, если это возможно. А символы `` (обратные кавычки) значат, что нужно выполнить команду внктри них, а потом подставить результат в то место, где указано выражение в обратных кавычках. Таким образом, команда FULLNAME="Veniamin Furman `rm -rf ./testdir`" выполнится так: 1. В переменную записывается строка Veniamin Furman 2. Выполняется команда rm -rf ./testdir 3. Вывод команды дописывается в переменную Соответственно, каталог ./testdir в процессе будет удален.
Общие рекомендации
Всегда держите в голове, что данные внутри eval$() будут исполнены bash’ем, как код (команды оболочки).
Исправление
1. Заменить двойные кавычки (") на одинарные ('). Строки в одинарных кавычках будут интерпретированы просто как текстовые последовательности, и без всякого выполнения и изменения записаны в переменные, так как есть: eval $(echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" sq $1 sq; print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq; print "WORKTIME=" sq $5 sq}' sq="'") Вывод: vfurry1 Veniamin Furman `rm -rf ./testdir` +415314499900 99 12:20-19:25 2. Но и это еще не конец, злоумышленник может обмануть и одинарные кавычки, обернув свою команду в …одинарные кавычки: vfurry1,Veniamin Furman '`rm -rf ./testdir`',+415314499900,99,12:20-19:25 Тогда выполнение будет происходить так. Когда очередь дойдет до FULLNAME awk добросовестно выведет: FULLNAME='Veniamin Furman '`rm -rf ./testdir`' eval же это также добросовестно интерпретирует. В $FULLNAME будет записано Veniamin Furman, далее закроется одинарная кавычка, т.е. больше ничего в переменную писаться не будет, а далее выполнится rm -rf ./testdir. Решение таково — удалить или заменить на что-либо безобидное, одинарные кавычки из входной строки перед передачей ее awk. Можно удалить sed‘ом: sed -e 's~'\''~~g' Код целиком: eval $(echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq; print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq; print "WORKTIME=" sq $5 sq}' sq="'") Вывод зловредной сткроки будет: vfurry1 Veniamin Furman `rm -rf ./testdir` +415314499900 99 12:20-19:25
Тестовый пример
Файлы: maketestdir — скрипт для создания тестовых каталогов. testfile.txt — Тестовый файл. Ошибку безопасности можно обойти, заменив двойные кавычки одинарными в коде. testfile2.txt — Замена двойных кавычек одинарными не поможет. Необходимо дополнительно удалять одинарные кавычки во входной строке. wrongcode — файл без исправлений кода (в коде используются двойные кавычки) wrongcode2 — только первое исправление (в коде используются одинарные кавычки) goodcode — окончательно исправленный код. Вызов скриптов: <скрипт> <тестовый файл> Например: wrongcode testfile2.txt Пример на GitHub]]>

Записать вывод awk в несколько переменных bash

Преамбула
Предположим, у нас есть некоторая таблица в виде файла CSV с набором полей, например таких Login,FullName,Phone,Room,WorkTime и разделителем полей , (запятая): verb666,Misha Verbitsky,+415314499922,42,11:00-16:00 ktvs421,Vasiliy Kotov,+415314499966,77a,00:00-06:00 dkldn89,Dmitry Kaledin,+415314499949,65b,22:00-00:00 vfurry1,Veniamin Furman,+415314499900,99,12:20-19:25 tpunk56,Tolik Punkoff,+415314499911,59,00:00-11:00 Нужно вытащить из нее некоторые данные, и далее как-либо обработать. Вытащить данные можно с помощью awk, используя оператор print, но возникает вопрос, как передать данные обратно в bash. Предположим, что заголовок удален, в файле остались только данные.
awk и eval
В bash есть встроенная команда eval, преобразующая переданную ей строку в команду или набор команд оболочки, и запускающая ее на выполнение. Этим и воспользуемся. 1. Организуем цикл, в котором будем производить обработку данных: IFS_=$IFS IFS=$'\n' for TMPSTRING in $(cat "demotable.txt") do #тут будет код done IFS=$IFS_ Перед циклом я подправил переменную $IFS содержащую глобальные разделители, в нее, в частности, «смотрят» операторы циклов, чтобы определить, где начинается следующий элемент. По умолчанию переменная $IFS содержит пробел, табуляцию и перевод строки, но поскольку у нас есть данные с пробелом, то это не подходит, цикл будет работать неверно. Потому сохраняем старое значение во временную переменную, устанавливаем новое значение в перевод строки (\n). После цикла возвращаем значение на место. В цикле организуем разбор данных: echo "$TMPSTRING"|awk -F "," '{print "LOGIN=" $1; print "FULLNAME=" $2 print "PHONE=" $3; print "ROOM=" $4; print "WORKTIME=" $5 }' Если запустить скрипт сейчас, то он выведет следующее: LOGIN=verb666 FULLNAME=Misha Verbitsky PHONE=+415314499922 ROOM=42 WORKTIME=11:00-16:00 Т.е. уже похоже на присваивание значений переменным bash, но есть проблема. Если мы сейчас скормим вывод awk eval‘у, то получим ошибку, например такую: ./awk2vars01: line 8: Verbitsky: command not found А если бы и не получили, то в переменных могла бы оказаться всякая ерунда, строки необходимо экранировать кавычками.
awk print и вывод кавычки
Кавычки для оператора print awk являются служебными символами, в двойные кавычки берутся строковые литералы, т.е. те строки, которые нужно вывести без изменений, как например, "LOGIN=" в коде выше, а в одинарные — вся программа awk. Экранирование (\" или \') в операторе print приведет к ошибке. Решение — завести внутреннюю переменную awk, содержащую кавычку, и печатать ее в нужном месте:
echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq;
    print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq; print"ROOM=" sq $4 sq;
    print "WORKTIME=" sq $5 sq}' sq="'"
Поскольку данные строки далее будут переданы в eval и обработаны как команды оболочки, то необходимо позаботиться о безопасности, и использовать только одинарные кавычки, а также удалять одинарные кавычки из входных строк, при передаче их awk: Об опасности использования eval в bash-скриптах. Копия Вывод: LOGIN='verb666' FULLNAME='Misha Verbitsky' PHONE='+415314499922' ROOM='42' Теперь можно обернуть все это в eval, чтобы раскидать результат работы awk по переменным.
eval $(echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "LOGIN=" sq $1 sq;
    print "FULLNAME=" sq $2 sq; print "PHONE=" sq $3 sq;
    print"ROOM=" sq $4 sq;
    print "WORKTIME=" sq $5 sq}' sq="'")
В демо-скрипте я просто вывожу данные на консоль, в реальном скрипте, что понятно, можно делать обработку данных в переменных bash. echo "Login: $LOGIN" echo "Full name: $FULLNAME" echo "Phone: $PHONE" echo "Room: $ROOM" echo "Work time: $WORKTIME" Вывод: Login: verb666 Full name: Misha Verbitsky Phone: +415314499922 Room: 42 Work time: 11:00-16:00 ... Скрипты полностью можно посмотреть на GitHub [lj-cut text=" Немного об оптимизации " unicancor="optimisation"]
Немного об оптимизации
На самом деле циклы в bash работают довольно медленно, и на реальной производственной задаче такой код довольно сильно тормозил, отрабатывая на таблице в 100 записей примерно 1 секунду:
IFS_=$IFS
IFS=$'\n'
J=0
for TMPSTRING in $(cat "data/servers")
do
let "J+=1"
#extract data
eval $(echo "$TMPSTRING"|sed -e 's~'\''~~g'|awk -F "," '{print "HOST_NAME="dq $1 dq;
     print "IP="dq $2 dq;print "SCORE=" dq $3 dq;print "PING=" dq $4 dq;
     print "SPEED=" dq $5 dq;print "COUNTRY=" dq $6 dq;
     print "COUNTRYSHORT=" dq $7 dq; print "NUMVPNSESSION=" dq $8 dq;
     print "UPTIME=" dq $9 dq;print "TOTALUSERS=" dq $10 dq;
     print "TOTALTRAFFIC=" dq $11 dq;print "LOGTYPE=" dq $12 dq;
     print "OPERATOR=" dq $13 dq;print "MSG=" dq $14 dq }' dq='"')
MENUSTR="\"$J $HOST_NAME($IP,$COUNTRYSHORT)\" \
\"$SCORE|$PING|$SPEED|$NUMVPNSESSION\" \
\"Uptime:$UPTIME Users:$TOTALUSERS Traffic:$TOTALTRAFFIC Log:$LOGTYPE\" \\"
echo "$MENUSTR" >> "vpnmenu.txt"
done
IFS=$IFS_
Его удалось оптимизировать до такого, без использования цикла и переменных bash:
cat data/servers | awk -F, \
'{
    HOST_NAME       = $1;
    IP              = $2;
    SCORE           = $3;
    PING            = $4;
    SPEED           = $5;
    COUNTRY         = $6;
    COUNTRYSHORT    = $7;
    NUMVPNSESSION   = $8;
    UPTIME          = $9;
    TOTALUSERS      = $10;
    TOTALTRAFFIC    = $11;
    LOGTYPE         = $12;
    OPERATOR        = $13;
    MSG             = $14;
    printf \
    "\"%i %s(%s,%s)\" \"%s|%s|%s|%s\"" \
    " \"Uptime:%s Users:%s Traffic:%s Log:%s\" \\\n",
    ++j, HOST_NAME, IP, COUNTRYSHORT, SCORE, PING, SPEED, NUMVPNSESSION,
    UPTIME, TOTALUSERS, TOTALTRAFFIC, LOGTYPE;
}' > vpnmenu.txt
Но в данном случае мне просто повезло, нужно было перекодировать данные из одного формата в другой.[/lj-cut] ]]>

Получение строки с определенным номером из файла в Linux

С помощью sed
sed -n 5p /path/to/file Получить 5 строку из текстового файла с путем /path/to/file
С помощью awk
awk 'NR == 5' /path/to/file На мой взгляд sed‘ом несколько проще, потому что не нужны лишние движения для подстановки переменных shell/bash-скрипта: TEXTFILE="/etc/group" STRNO=5 TMPSTRING=`sed -n "$STRNO"p "$TEXTFILE"`]]>

Подключение Riseup VPN в Windows XP

Приходится кое-где сидеть на старой машине с XP (хорошо, не с 2000), а безопасно посидеть в интернете хочется. Так что от склероза, и мало ли кому надо будет. Аналогичным образом можно подключить на XP и VPN от calyx.net

Подготовка
1. Необходимо установить OpenVPN для Windows XP: Скачать с официального сайта: — Версия x86 (32-разрядная)Версия x64 (64-разрядная) Копии, на случай, если с официального сайта пропадет: — Версия x86 (32-разрядная)Версия x64 (64-разрядная) 2. Далее, качаем конфиги: Для Riseup: — C Mega.nzC Google.Drive Для Calyx: — C Mega.nzC Google.Drive 3. Конфиги необходимо распаковать в каталог для файлов конфигурации OpenVPN (C:\Program Files\OpenVPN\config)
Получение ключей и сертификатов
Получение ключей для VPN сделано обычным BAT-файлом, без проверок fingerprint’а сертификата провайдера, а в wget, которым все и выкачивается, указан ключ --no-check-certificate, так что все это дело не сильно секурно. 1. Скачиваем архив с GitHub На всякий случай копия на Google.Drive и Mega.nz 2. Распаковываем архив в отдельный каталог, пусть будет C:\GetKeys 3. Запускаем командную строку (Пуск —> Выполнить и вводим cmd) 4. Переходим в каталог: С: cd \GetKeys 5. Запускаем получение ключей. Для Riseup: getkeys.bat riseup.net Для Calyx, соответственно: getkeys.bat calyx.net После того, как батник отработает, в C:\GetKeys образуется подкаталог data с подкаталогами провайдеров. Из C:\GetKeys\data\riseup.net надо скопировать в C:\Program Files\OpenVPN\config файлы cacert.pem и openvpn.pem для Riseup VPN, а для Calyx из C:\GetKeys\data\calyx.net те же файлы, переименовав их, соответственно как cacert_calyx.pem и openvpn_calyx.pem. Или можете отредактировать конфиг calyx-net.ovpn и задать там любые желательные имена. Теперь можно запускать OpenVPN GUI, выбирать, щелкнув по иконке в трее, нужный конфиг, и соединяться.
Проверка
До соединения:
Riseup:
[lj-cut text="Calyx" unicancor="calyx"]Calyx:
[/lj-cut]]]>

Получение ключей и сертификатов для Riseup VPN и calyx.net в Windows.

Без использования клиента Riseup или Bitmask для Calyx.

Преамбула
Наскриптил тут утилитку, которая выкачивает ключи для Riseup VPN копия, а заодно уж и для calyx копия под Windows. Изначально писалось для XP, т.к. оказалось, что официальный клиент Riseup в XP не работает, а Calyx вообще использует Bitmask, которого для винды не предвидится. Моя утилита тоже, как оказалось, не всегда работает в XP, но для XP я таки придумал, как выгрузить ключи и подключиться к Riseup, но об этом в другой раз. Так что получилась такая утилита, чтобы подключаться к Riseup и Calyx не качая официального клиента. Пусть уж будет.
Порядок действий
1. Ставим OpenVPN 2. Качаем виндоконфиги: Для Riseup: — C Mega.nzC Google.Drive Для Calyx: — C Mega.nzC Google.Drive 3. Распаковываем конфиги в каталог конфигов OpenVPN (Обычно C:\Users\<имя_пользователя>\OpenVPN\config\). 4. Качаем утилиту отсюда и распаковываем в отдельный каталог. Она портабельная, никакой установки не надо. 5. Запускаем. 6. Выбираем в выпадающем списке провайдера. 7. Жмем на кнопку «Получить ключ пользователя» Если данных и сертификата провайдера нет, то программа сначала получит данные провайдера, а потом пользователя. Если данные провайдера есть, то обновится только пользовательский ключ. Если в дальнейшем надо будет принудительно обновить данные провайдера, жмем кнопку «Обновить данные провайдера» Примечание: Если надо добавить нового провайдера, то дописываем адрес без http://, https:// и www в файл providers.txt в подкаталоге data, по адресу на строку. Должно получиться как на скриншоте:
8. Жмем кнопку «Сохранить ключи…» и сохраняем их в каталог с конфигами OpenVPN. Имена ключей для Riseup оставляем без изменений, а для Calyx сохраняем как cacert_calyx.pem и openvpn_calyx.pem. Или можете отредактировать конфиг calyx-net.ovpn и задать там любые желательные имена. Репозиторий на GitHub]]>

Полезные фишки в BAT/CMD файлах.

Выход из BAT-файла
exit /B Ключ /B обязателен! Если использовать просто exit — закроется командный интерпретатор.
Проверка, пустая ли переменная
Можно использовать для проверки параметров в командной строке.
if "%~1"=="" (
	echo Use %0 ^<address^>
	echo Address must be without http://, https:// or www
	echo e.g. riseup.net
	pause
	exit /B
)
Запись вывода программы в переменную
Аналог VAR=`program` в bash. for /f %%i in ('program') do set "API_URI=%%i"& goto f1 :f1 Пример: for /f %%i in ('bin\jq .api_uri %WORKDIR%/provider.json') do set "API_URI=%%i"& goto f1 :f1 Источник
SetLocal и расширенная обработка команд
Статья на Киберфоруме Копия в PDF
Экранирование спецсимволов
В BAT/CMD файле некоторые символы (перенаправления > и <, конвейера |, символ &, указание переменной %) считаются специальными. Если символ должен быть включен в команду как символ, могут быть глюки. Перед символом нужно указать символ экранирования: ^ (крышку). Пример: echo Use batfile.bat ^<address^> (выведет на экран Use batfile.bat <address>) Источник Копия в PDF Там есть и другие полезные штуковины.
Удаление определенного символа из строки.
Например, надо удалить из переменной %VAR% все кавычки: set VAR=%VAR:"=% Источник Копия в PDF В источнике есть и другие примеры работы со строками.]]>

C#, HttpWebRequest: использование самоподписанного (self-signed) сертификата.

Преамбула
Если попытаться соединиться с HTTPS-сервером, имеющим самоподписанный сертификат, то HttpWebRequest сгенерирует исключение WebException Базовое соединение закрыто: Не удалось установить доверительные отношения для защищенного канала SSL/TLS. (The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.). Оно и понятно, HttpWebRequest не смог проверить сертификат узла и закономерно нас послал. Решение заключается в переопределении глобального обработчика System.Net.ServicePointManager.ServerCertificateValidationCallback. Внимание! Изменение ServerCertificateValidationCallback повлияет на все соединения в программе, пока обработчик не будет установлен по умолчанию, надо не забывать это, и возвращать обработчик к стандартному значению, после того, как работа с сервером, имеющим самоподписанный сертификат, завершена. Предположим, есть класс-обертка над HttpWebRequest, надо его дополнить так, чтоб можно было работать с серверами с self-signed сертификатами.
Игнорирование неправильных сертификатов
Самый простой способ, но не самый безопасный. ServerCertificateValidationCallback передается делегат, который всегда возвращает true. Добавляем в класс функцию:
public void EnableIgnoreCertError()
{
    ServicePointManager.ServerCertificateValidationCallback =
        delegate { return true; };
}
И функцию, которая возвращает ServerCertificateValidationCallback к значению по умолчанию:
public void DisableIgnoreCertError()
{
    ServicePointManager.ServerCertificateValidationCallback =
        null;
}
Начали работать с сервером — вызвали первую, закончили — вызвали вторую.
Самостоятельная проверка сертификата
Немного более сложный, но более безопасный способ. Необходимо заранее либо иметь сам сертификат, либо знать его хэш. В следующем примере будем проверять как раз хэш SHA1 сертификата. 1. Подключим в References’ах System.Security.Cryptography. 2. Подключим необходимые пространства имен: using System.Net.Security; using System.Security.Cryptography.X509Certificates; 3. Заведем в классе поле, куда из вызывающей программы будем передавать заранее известный хэш сертификата: public string CertHashString { get; set; } 4. Заведем две функции, для включения и выключения самостоятельной проверки сертификата:
public void EnableValidateCert()
{
    ServicePointManager.ServerCertificateValidationCallback =
        ValidateCert;
}
public void DisableValidateCert()
{
    ServicePointManager.ServerCertificateValidationCallback =
        null;
}
В первой функции вместо делегата, возвращающего true, указываем нашу функцию проверки.
Функция проверки сертификата
Функция проверки сертификата обязательно должна иметь следующий заголовок: bool FunctionName (object, X509Certificate, X509Chain, SslPolicyErrors), т.е., например:
private bool ValidateCert(object sender, X509Certificate cert,
    X509Chain chain, SslPolicyErrors sslPolicyErrors)
В функции: 1. Проверяем наличие ошибок SSL, если их нет, значит сертификат уже был опознан:
if (sslPolicyErrors == SslPolicyErrors.None)
{
    return true;
}
2. Если известный хэш сертификата не был передан вызывающей программой, значит и проверять нечего:
if (string.IsNullOrEmpty(CertHashString))
{
    return false;
}
3. Получаем хэш сертификата сервера в виде строки: string hashstring = cert.GetCertHashString(); 4. Если хэши полученного и известного сертификата совпадают — все ок, возвращаем true, иначе false.
if (hashstring == CertHashString)
{
    return true;
}
return false;
Функция целиком:
private bool ValidateCert(object sender, X509Certificate cert,
    X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    //все и так хорошо
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }
    //не передан хэш сертификата
    //значит и проверять нечего
    if (string.IsNullOrEmpty(CertHashString))
    {
        return false;
    }
    //получаем хэш сертификата сервера в виде строки
    string hashstring = cert.GetCertHashString();
    //если хэши полученного и известного
    //сертификата совпадают - все ок.
    if (hashstring == CertHashString)
    {
        return true;
    }
    return false;
}
Класс целиком на PasteBin На GitHub Источник]]>

C#, получение хэша (fingerprint) сертификата X509.

Преамбула
Понадобилось проверить сертификат X509 на целостность и подлинность. Поставщик сертификата отдельно передает его fingerprint и алгоритм хэширования по которому тот вычисляется. Судя по документации от MS, получить fingerprint, он же хэш, можно функцией GetCertHashString() (или GetCertHash() в виде массива байт) класса X509Certificate2, однако, в .NET 2.0 отсутствует перегрузка функции GetCertHashString(HashAlgorithmName), которая позволяет выбрать алгоритм хеширования (GetCertHashString() возвращает хэш SHA1). Потому сделаем руками, благо ничего сложного в этом нет.
Получение fingerprint
1. Подключаем в References System.Security.Cryptography 2. Подключаем необходимые пространства имен в классе: using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; 3. Загружать сертификат будем из файла, потому проверяем его наличие:
if (!File.Exists(CertFile))
{
    Console.WriteLine("File not found.");
    return null;
}
4. Загружаем сертификат (создаем экземпляр класса X509Certificate2). Если вдруг файл сертификата поврежден, то конструктор X509Certificate2 вызовет Exception Требуемый объект не найден. Поэтому конструктор надо обернуть в try...catch:
try
{
    X509 = new X509Certificate2(CertFile);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    return null;
}
5. Получаем загруженный сертификат в виде массива байт: byte[] cert = X509.GetRawCertData(); 6. Создаем объект HashAlgorithm, который и займется вычислением хэша/fingerprint’а. Я завел в функции входную переменную AlgName, куда записывается строка с названием алгоритма, что бы иметь возможность выбора алгоритма хэширования если что:
HashAlgorithm alg = null;
switch (AlgName.ToUpperInvariant())
{
    case "MD5": alg = MD5.Create(); break;
    case "SHA1": alg = SHA1.Create(); break;
    case "SHA256": alg = SHA256.Create(); break;
    case "SHA384": alg = SHA384.Create(); break;
    case "SHA512": alg = SHA512.Create(); break;
    default:
        {
            Console.WriteLine("Unknow algorithm.");
            return null;
        }
}
7. Получаем хэш в виде массива байт: byte[] hash = alg.ComputeHash(cert); 8. Преобразуем массив байт в строку шестнадцатеричных чисел:
string hex = BitConverter.ToString(hash).ToLowerInvariant().
    Replace("-", "");
ToLowerInvariant() преобразует буквы в строке, полученной BitConverter в строчные, а Replace("-", "") удаляет разделители (-) между значениями байтов, которые BitConverter вставляет в строку. Код функции целиком Тестовый пример на GitHub]]>