Дёргаем цепочку сертификатов

    Вчера, видимо, был шабаш https и клиенты стали массово слать сертификаты. Разумеется ни корневых ни промежуточных не прилагалось и просьба их выслать вызывала такое же недоумение как встречный поток у блондинки на дороге с односторонним движением.

    На 4-м сертификате дёргать их вручную стало лень (а я ленив по натуре), поэтому набросал «самокат» выцепляющий издателя и формирующий chain-файл для скармливания nginx'у.
    Наверняка он не идеален и проверен лишь на полуторадесятках сертификатов, но чем богаты.

    Об устройстве x.509 много сказано (в том числе на хабре), поэтому повторяться не буду.

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

    Всё нижесказанное актуально для:

    Скрытый текст
    $ uname -or
    FreeBSD 10.3-STABLE
    $ openssl version
    OpenSSL 1.0.2h  3 May 2016
    $ `echo $SHELL` --version
    tcsh 6.18.01 (Astron) 2012-02-14 (x86_64-amd-FreeBSD) options wide,nls,dl,al,kan,sm,rh,color,filec
    $ /usr/local/bin/bash --version
    GNU bash, version 4.3.25(1)-release (amd64-portbld-freebsd10.0)
    


    Итак, предположим, что у нас есть PEM-сертификат сайта. Для примера мы возьмём сертфикат ya.ru (не только ж пинговать его).

    $ echo | openssl s_client -connect ya.ru:443 | openssl x509 -certopt ca_default -out ya.pem -outform PEM
    

    Помимо самого кодированного запроса, версии, подписи и т.п. в нём имеется ряд расширений. Одно из которых Authority Information Access нас и интересует:

    $ openssl x509 -in ./ya.pem -noout -text | grep 'Authority Information Access' -A 2
                Authority Information Access:
                    OCSP - URI:http://yandex.ocsp-responder.com
                    CA Issuers - URI:http://repository.certum.pl/ycasha2.cer
    

    Параметр CA Issuers как раз и содержит следующий в цепочке сертификат. Как правило, данный сертификат либо в PEM, либо в DER(как в нашем случае) форматах.

    $ fetch http://repository.certum.pl/ycasha2.cer
    

    На деле PEM формат не более чем base64 представление DER и получить PEM из DER можно сделав base64 ./ycasha2.cer ./ycasha2.pem и обрамив кодированный текст "-----BEGIN CERTIFICATE-----","-----END CERTIFICATE-----". Однако, логичнее и проще сделать это преобразование средствами openssl:

    $ openssl x509 -inform der -in ./ycasha2.cer -out ./ycasha2.pem
    

    Едем дальше и смотрим следующий сертификат в цепочке:

    $ openssl x509 -in ./ycasha2.pem -noout -text | grep 'Authority Information Access' -A 2
                Authority Information Access:
                    OCSP - URI:http://subca.ocsp-certum.com
                    CA Issuers - URI:http://repository.certum.pl/ctnca.cer
    

    $ fetch http://repository.certum.pl/ctnca.cer
    

    Преобразовываем и его:

    $ openssl x509 -inform der -in ./ctnca.cer -out ./ctnca.pem
    

    В данном сертификате (т.к. он корневой) отсутствует расширение Authority Information Access:

    $ openssl x509 -in ./ctnca.pem -noout -text | grep 'X509v3 extensions' -A 6
            X509v3 extensions:
                X509v3 Basic Constraints: critical
                    CA:TRUE
                X509v3 Subject Key Identifier:
                    08:76:CD:CB:07:FF:24:F6:C5:CD:ED:BB:90:BC:E2:84:37:46:75:F7
                X509v3 Key Usage: critical
                    Certificate Sign, CRL Sign
    

    То есть на нём и закончим вытягивание цепочки. Осталось собрать это всё в chain-файл:

    $ cat ya.pem ycasha2.pem ctnca.pem > chain0.pem
    

    Вроде бы теперь можно ставить (если есть Private Key), но остановлюсь ещё на паре нюансов.
    Установив свой сертификат на свой Яндекс проверяем его:

    $ echo | openssl s_client -connect ya.ru:443 | grep Verify
        Verify return code: 0 (ok)
    

    Всё хорошо, но это лишь потому, что в дефолтных путях -CApath, -CAfile моего openssl нашлись нужные хеши сертификатов. Если мы их изменим, либо по дефолтным путям их нет, либо они просто устарели, либо у кого-то версия openssl с багом, в которой не «цеплялись» default CApath (если не ошибаюсь с 1.0.1с по 1.0.1e), то получим неприятность в виде:

    $ echo | openssl s_client -connect ya.ru:443 -CApath . | grep Verify
        Verify return code: 20 (unable to get local issuer certificate)
    

    Понятно, что корневой сертификат подписать некому, поэтому нужно нашей системе разрешить доверять ему. Для этого можно создать кусочек хранилища. При поиске требуемого сертифката openssl пытается отыскать его по хешу сертификата.

    $ openssl x509 -noout -hash -in ./ctnca.pem
    48bec511
    

    $ ln -s `pwd`/ctnca.pem `pwd`/48bec511.0
    

    И теперь наша система доверяет ya.ru:

    $ echo | openssl s_client -connect ya.ru:443 -CApath . | grep Verify
    DONE
        Verify return code: 0 (ok)
    

    Разумеется делать руками каждый раз лень, потому слегка автоматизируем:

    $ cat ./issuers.sh
    #!/usr/local/bin/bash
    
    cmd_grep='/usr/bin/grep '
    cmd_openssl='/usr/bin/openssl '
    cmd_cut='/usr/bin/cut '
    cmd_fetch='/usr/bin/fetch '
    
    
    tmp_der='tmp.der'
    tmp_cert='tmp.cert'
    
    #------------------------------------------------------------------------------
    usage () {
        #printf "function ${FUNCNAME}\n"
        printf "Error!\nUsage:\t\"$0 certificate.pem\"\n"
        exit 1
    }
    #------------------------------------------------------------------------------
    if [ "X$1" = "X" ]
    then
        usage
    else
        cp $1 $tmp_cert
        chain_cert="chain.pem"
    fi
    
    i=0
    while :
    do
        issuer=`$cmd_openssl x509 -in $tmp_cert -noout -text | $cmd_grep 'CA Issuers' | $cmd_cut -d : -f 2,3`
        if [ "X$issuer" != "X" ]
        then
            echo $i
            echo $issuer
            tmp_pem=$1$i.pem
            $cmd_fetch $issuer --output=$tmp_der
            is_pem=`$cmd_grep -c CERTIFICATE $tmp_der`
            printf "IS PEM:\t[$is_pem]\n"
            #echo "$tmp_der -> $tmp_pem"
            if [ $is_pem -ne 0 ]
            then
                echo "PEM($tmp_der) -> PEM($tmp_pem)"
                cp -f $tmp_der $tmp_pem
            else
                echo "DER($tmp_der) -> PEM($tmp_pem)"
                echo "$cmd_openssl x509 -inform der -in $tmp_der -out $tmp_pem"
                $cmd_openssl x509 -inform der -in $tmp_der -out $tmp_pem
            fi
            cp $tmp_pem $tmp_cert
            let "i+=1"
            #sleep 2
        else
            break
        fi
    done
    
    if [ $i -gt 0 ]
    then
        echo "cat ./$1* > $chain_cert"
        cat ./$1* > $chain_cert
        printf "Certificate chain:\n"
        ls -l $chain_cert
        #ls | grep -Ev ^ya.pem$ | xargs rm
    fi
    


    Выполняем:

    $ ./issuers.sh ./ya.pem
    0
    http://repository.certum.pl/ycasha2.cer
    tmp.der                                       100% of 1196  B   16 MBps 00m00s
    IS PEM: [0]
    DER(tmp.der) -> PEM(./ya.pem0.pem)
    /usr/bin/openssl  x509 -inform der -in tmp.der -out ./ya.pem0.pem
    1
    http://repository.certum.pl/ctnca.cer
    tmp.der                                       100% of  959  B   13 MBps 00m00s
    IS PEM: [0]
    DER(tmp.der) -> PEM(./ya.pem1.pem)
    /usr/bin/openssl  x509 -inform der -in tmp.der -out ./ya.pem1.pem
    cat ././ya.pem* > chain.pem
    Certificate chain:
    -rw-r--r--  1 root  wheel  5842 Jun 30 15:46 chain.pem
    

    Сверяем показания:

    $ md5 chain0.pem ; md5 chain.pem
    MD5 (chain0.pem) = 6d32b0798d48d14764cd26cc4f730444
    MD5 (chain.pem) = 6d32b0798d48d14764cd26cc4f730444
    

    Как-то так… Разумеется скрипт не универсален, всё на скорую руку в предверии грандиозного шухера. Комментарии/пожелания приветствуются, но отвечать вряд ли смогу — у нас тут (в Беларуси) дурдом деноминация.
    • +14
    • 15,4k
    • 3
    Поделиться публикацией

    Комментарии 3

      0
      А как быть в ситуации, когда сертификат подписан сразу двумя сертификатами. Например, Крипто Про выдает сертификаты, которые первично подписаны самоподписанным корневым сертификатом Крипто Про, но у него еще существует подпись и от цепочки с началом у МинСвязи РФ (что и дает сертификату юридическую силу). При попытке вытащить цепочку, у меня вытаскивался именно первый, самоподписанный сертификат Крипто Про.
        0
        Не работал с криптопро. На сколько понимаю они в pfx или pb7? В openssl для этих посткоманд не предусмотрен парсинг сведений, т.е. опция -text недоступна.
        Можно попробовать сделать, например, так (для pfx):
        $ openssl pkcs12 -in cert.pfx | openssl x509 -noout -text
        

        вероятно запросит пароли.
        или так без извлечения ключа:
        $ openssl pkcs12 -in cert.pfx -nokeys | openssl x509 -noout -text
        
        –1
        Ну у КриптоПро какая-то своя хитрая реализация хранилища PKCS#7, а в статье про openssl.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое