Pull to refresh

Автоматическое создание SSL-сертификата для нескольких доменов подписанного внутренним CA

Проблема


Использование SSL-сертификатов — обычное дело во многих проектах. Проблема в том, что зачастую нет возможности использовать «честный» сертификат, подписанный известным Certificate Authority (CA). Обычно на стадии разработки продукта используются самоподписанные (Self-Signed) сертификаты. Однако, этот вполне зарекомендовавший себя подход имеет ряд недостатков:
— браузеры и прочие программы не доверяют самоподписанным сертификатам. Так, в случае браузеров, приходится часто обрабатывать появляющиеся предупреждения о подозрительном сертификате.
— если в случае браузера можно проигнорировать предупреждение, то некоторые программы отказываются работать с подозрительным сертификатом наотрез (например, Outlook при обращении к Exchange серверу)
— в принципе проблема решается добавлением сертификата в подходящее хранилище браузера или операционной системы (например, Trusted Root Certificates в Windows). Но если приходится работать с множеством сайтов и сертификатов, то сам по себе импорт сертификата в хранилище становится частой и надоедливой задачей.
— проблемы в проекте, связанные с сертификатами, игнорируются — ведь раз используются заведомо невалидные сертификаты, то и ожидать, что они будут адекватно работать не приходится. Это может привести к пропущенным ошибкам в ПО.

Решение


Из ситуации можно выходить по-разному, на хабре уже были статьи описывающие некоторые подходы (один, два, три). Я хочу описать другой подход, основанный на использовании внутренного CA.

Итак, предоложим, что нам
— нужно часто генерить SSL-сертификаты для различных доменов, устанавливать созданные сертификаты на некоторые сервисы и обращаться к этим сервисам с помошью иных программ.
— не хочется сталкиваться с проблемами, обусловленными самоподписанностью созданных сертификатов.
— нужно тратить минимум сил и времени на создание сертификатов и обход проблем, связанных с их «нечестностью».

Собственно, решение очень простое и основано на двух идеях:
— нужно использовать внутренний CA
— нужно автоматизировать процесс создания сертификата, подписанного внутренним CA.

Чем нам поможет автоматизация создания сертификатов, думаю и так ясно. Поэтому добавлю пару слов только по первому пункту — что такое внутренний CA и чем он в нашей ситуации полезен.
Внутренний CA — это Certificate Authority, установленный и работающий где-то в интранете. Ему можно слать запросы на сертификаты (certificate requests), а он умеет их подписывать своим собственным сертификатом. Основное отличие этого внутреннего CA от публичных (например, Verisign) заключается в том, что сертификат, который использует для подписи Verisign, (точнее — его публичная часть) известен всему миру и предустановлен на большинстве ОС и программ. Про внутренний же CA никто не знает, и соответственно доверять SSL сертификатам, выпущенным вашим CA, по умолчанию не станет. Лечится эта проблема так же, как и недоверие к самоподписанным сертификатам — импортом в хранилище сертификатов доверенных CA. Но делать этот обходной маневр придется единственный раз, а не для каждого вновь созданного сертификата. Экономия сил и времени налицо.

Пример использования


Опишу пример организации такой системы. Сразу оговорюсь, что наверняка ее можно было бы сделать попроще (например не мешать Windows и Linux, а использовать только один какой-то тип ОС), текущая схема работы сложилась во многом исторически.
Итак, нам потребуется
— Windows 2003 Server с установленной Active Directory, компонентой Certificate Services и PowerShell.
— Linux'овая машинка с установленным python'ом (у нас используется версия 2.3) и openssl
— Дополнительный модуль pexpect для python'а (http://www.noah.org/wiki/pexpect)
— Telnet-доступ с Linux'овой машинки на w2k3 сервер
— Скрипт, который сделает за нас половину работы.

AD на виндовом хосте нужна для того, чтобы установить Enterprise root CA. В принципе, возможно, что для работы достаточно и Stand-alone root CA — я просто не проверял. В любом случае, можно создать фейковый домен AD с единственным контроллером домена и поставить на него Certificate Services — будет прекрасно работать.

Linux и python — просто использовалось то, что уже знакомо. Без сомнения, все нужные действия можно было бы реализовать и на виндовых технологиях.

Telnet — опять же, мудрить не хотелось, а во внутренней сети на серверах для внутренних нужд не все ли равно.

Работает все это хозяйство просто: w2k3 сервер просто работает и ждет, когда кто-нибудь попросит у него сертификат. На Linux'овом хосте лежит скрипт, который принимает на вход список доменных имен, для которых должен быть выписан сертификат. На выходе скрипт генерит набор файлов:
— запрос на создание сертификата, который можно скормить любому CA
— приватный ключ сертификата (в шифрованном виде и без шифрования)
— сертификат в формате PEM
— сертификат в формате PFX (с «оригинальным» паролем для импорта 123qwe)
Для поданного на вход списка доменов создается один сертификат — все указанные домены перечисляются в качестве альтернативных имен (Subject Alternative Name). Соответственно созданный сертификат может использоваться для любого из перечисленных доменов.

Пример запуска скрипта


Примерно так выглядит работа скрипта. По завершении работы можно брать готовые файлы сертификатов из папки, где лежит скрипт.
-bash-2.05b$ ./gencert example.com another.ru
Generating config file openssl.cnf from template openssl.cnf.template
Generating private key to file server.example.com.another.ru.key
Generating non-encripted private key to file server.example.com.another.ru.key.nosecure
Generating certificate request to file server.example.com.another.ru.csr
Processing certificate request server.example.com.another.ru.csr on CA host 10.63.10.10
Issued certificate is stored to file     server.example.com.another.ru.cer
Generating PEM certificate to file server.example.com.another.ru.pem
Generating PFX certificate to file server.example.com.another.ru.pfx


Generated files:
   Encrypted private key:                   server.example.com.another.ru.key
   Decrypted private key:                   server.example.com.another.ru.key.nosecure
   Certificate request:                     server.example.com.another.ru.csr
   Issued certificate (PEM Format):         server.example.com.another.ru.cer
   Issued certificate (PKCS#12/PFX Format): server.example.com.another.ru.pfx
You have new mail in /var/spool/mail/pbulenko
-bash-2.05b$


Немножко кода


Напоследок — сам скрипт и сопутствующий ему конфиг лдя openssl.

openssl.cnf.template (скрипт предполагает, что этот конфиг лежит рядом — в той же папке, где и сам скрипт).
HOME                    = .
RANDFILE                = $ENV::HOME/.rnd
oid_section             = new_oids

[ CA_default ]
policy          = policy_match

[ policy_match ]
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits            = 1024
default_keyfile         = privkey.pem
distinguished_name      = req_distinguished_name
attributes              = req_attributes

string_mask = nombstr

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = RU
countryName_min                 = 2
countryName_max                 = 2

stateOrProvinceName             = State or Province Name (full name)
stateOrProvinceName_default     = MyProvince

localityName                    = Locality Name (eg, city)
localityName_default            = MyCity

0.organizationName              = Organization Name (eg, company)
0.organizationName_default      = MyCompany

organizationalUnitName          = Organizational Unit Name (eg, section)
organizationalUnitName_default  = MyDept

commonName                      = Common Name 01 (eg, your name or your server\'s hostname)
commonName_max                  = 64


emailAddress                    = Email Address
emailAddress_max                = 64
emailAddress_default            = my@mailbox.com

[ req_attributes ]
challengePassword               = A challenge password
challengePassword_min           = 4
challengePassword_max           = 20
unstructuredName                = An optional company name

[ usr_cert ]
basicConstraints=CA:FALSE
nsComment                       = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer:always

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = CA:true

[ crl_ext ]
authorityKeyIdentifier=keyid:always,issuer:always

[ req_ext ]
subjectAltName          = @alt_names

[alt_names]


Сам скрипт (в функции execute_telnet_command вероятно придется заменить пароль по умолчанию на виндовый хост):
#!/usr/bin/python

import sys, os, shutil
import pexpect


def pexpect_try_expect(ch, text, time=60):
        out=""
        error=""
        mtext='pexpect.EOF'
        if text != pexpect.EOF:
                mtext=text.replace('\n', '\\n')
        try:
                w = ch.expect([text, pexpect.EOF, pexpect.TIMEOUT], timeout=time)
                if w == 0:
                        txt = text + '\n'
                        out=ch.before + txt
                elif w == 1:
                        txt = '\n\nExit waiting for text "%s" with pexpect.EOF message\n\n' % mtext
                        out=ch.before + txt
                elif w == 2:
                        txt = '\n\nFailed to wait for text "%s" with pexpect.TIMEOUT message; timeout is %s\n\n' % (mtext, time)
                        out=ch.before + txt
        except Exception, err:
                if text != pexpect.EOF:
                        print("ERROR: Waiting for text '%s' failed with %s" % (mtext, ch.after))
                        error="ERROR: Waiting for text '%s' failed with %s" % (mtext, ch.after)
                        if ch.before == None:
                                out=text + '\n'
                        else:
                                out=ch.before + text + '\n'
                else:
                        print("ERROR: Waiting for text '%s' failed with pexpect.EOF" % text  )
                        out=ch.before + '\npexpect.EOF\n'
                        error="ERROR: Waiting for text '%s' failed with %s" % ('pexpect.EOF', ch.after)
                err_details="""Error details:
        ==============================================
%s
        ==============================================
        """ % str(err)
                print('%s' % err_details)
        return out, error




def execute_telnet_command(ip, cmd, time=60, login='Administrator', passwd='123qwe'):
#        print('Execute command "%s" in telnet session on host %s as %s' % (cmd, ip, login))
        child = pexpect.spawn ('telnet %s' % ip)
        out, err = pexpect_try_expect(child, "login: ", 60)
        output = out+err

        child.send(login+'\r\n')
        out, err = pexpect_try_expect(child, "assword: ", 60)
        output=output+out+err

        child.send(passwd+'\r\n')
        out, err = pexpect_try_expect(child, "Administrator>", 60)
        output=output+out+err

        for elem in cmd:
                child.send(elem+'\r\n')
                out, err = pexpect_try_expect(child, "Administrator>", time)
                output=output+out+err

        child.send('exit\r\n')
        out, err = pexpect_try_expect(child, pexpect.EOF, 60)
        output=output+out+err

        return output



def generatePK(confwork, pk):
        if os.path.exists(pk):
                os.remove(pk)
        print("Generating private key to file %s" %pk)
        child = pexpect.spawn('openssl genrsa -des3 -out ' +pk + ' 1024' )
        pexpect_try_expect(child, "Enter pass phrase for "+pk+":", time=60)
        child.send('1q2w3e\n')
        pexpect_try_expect(child, "Verifying - Enter pass phrase for "+pk+":", time=60)
        child.send('1q2w3e\n')
        pexpect_try_expect(child, pexpect.EOF, time=60)


def generateNonEncriptedPK(pk, pknosecure):
        if os.path.exists(pknosecure):
                os.remove(pknosecure)
        print("Generating non-encripted private key to file %s" %pknosecure)
        child = pexpect.spawn('openssl rsa -in %s -out %s' %(pk,pknosecure))
        pexpect_try_expect(child, "Enter pass phrase for ", time=60)
        child.send('1q2w3e\n')
        pexpect_try_expect(child, pexpect.EOF, time=60)



def generateCertRequest(pk, crtreq, confwork):
        if os.path.exists(crtreq):
                os.remove(crtreq)
        print("Generating certificate request to file %s" % crtreq)
        child = pexpect.spawn('openssl req -new -nodes -key %s -out %s -config %s' %(pk,crtreq,confwork))
        pexpect_try_expect(child, "Enter pass phrase for ", time=60)
        child.send('1q2w3e\n')
        #pexpect_try_expect(child, "Country Name (2 letter code) [RU]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "State or Province Name (full name) [MyProvince]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "Locality Name (eg, city) [MyCity]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "Organization Name (eg, company) [MyCompany]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "Organizational Unit Name (eg, section) [MyDept]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "Common Name 01 ", time=60)
        pexpect_try_expect(child, ":", time=60)
        #print(sys.argv[1] + "\n")
        child.send(sys.argv[1] + "\n")
        #pexpect_try_expect(child, "Email Address [my@mailbox.com]", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "A challenge password ", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        #pexpect_try_expect(child, "An optional company name ", time=60)
        pexpect_try_expect(child, ":", time=60)
        child.send('\n')
        pexpect_try_expect(child, pexpect.EOF, time=60)




def generateRequest(conftempl, sanlist):
        confwork="openssl.cnf"

        print("Generating config file %s from template %s" %(confwork, conftempl))
        if os.path.exists(confwork):
                os.remove(confwork)
        shutil.copy(conftempl, confwork)

        fw = open(confwork, "a")
        c=1
        suffix=""
        for elem in sanlist:
                fw.write("DNS.%s    = %s\n" % (c, elem))
                if c<3:
                        suffix=suffix+'.'+elem.replace('*', 'wildcard')
                c=c+1
        fw.close()

        pk="server"+suffix+".key"
        pknosecure="server"+suffix+".key.nosecure"
        crtreq="server"+suffix+".csr"

        generatePK(confwork, pk)
        generateNonEncriptedPK(pk, pknosecure)
        generateCertRequest(pk, crtreq, confwork)

        return pk, pknosecure, crtreq



def processCertificateRequest(crtreq, cahost):
        cert = crtreq.replace('.csr', '.cer')
        print("Processing certificate request %s on CA host %s" %(crtreq,cahost))

        cmd = ['del c:\\temp\\%s' % crtreq]
        f = open(crtreq, 'r+')
        line = f.readline()
        while line:
                cmd.append('echo %s >> c:\\temp\\%s' %(line.replace('\n',''), crtreq))
                line = f.readline()
        f.close()
        cmd.append('del c:\\temp\\%s && certreq -submit -config -  -attrib "CertificateTemplate: WebServer" c:\\temp\\%s c:\\temp\\%s' %(cert,crtreq,cert))
        execute_telnet_command(cahost, cmd, 120)

        cmd=['powershell foreach ($elem in get-content c:\\temp\\%s) {$elem}' % cert]
        res = execute_telnet_command(cahost, cmd, 120)

        splitted=res.split('\r\n')
        while splitted[0] != "-----BEGIN CERTIFICATE-----":
                splitted.remove(splitted[0])

        if os.path.exists(cert):
                os.remove(cert)

        f=open(cert, 'w')
        for elem in splitted:
                f.write(elem+'\n')
                if "-----END CERTIFICATE-----" == elem:
                        break;
        f.close()
        print("Issued certificate is stored to file     %s" %cert)

        return cert



def generatePFXCertificate(cert, pknosecure):
        certpfx = cert.replace('.cer', '.pfx')
        certpem = cert.replace('.cer', '.pem')

        print("Generating PEM certificate to file %s" %certpem)
        fr = open(cert, 'r')
        fw = open(certpem, 'w')

        line = fr.readline()
        while line:
                fw.write(line)
                line = fr.readline()

        fr.close()
        fr = open(pknosecure, 'r')
        line = fr.readline()
        while line:
                fw.write(line)
                line = fr.readline()

        fr.close()
        fw.close()

        if os.path.exists(certpfx):
                os.remove(certpfx)
        print("Generating PFX certificate to file %s" %certpfx)
        child = pexpect.spawn('openssl pkcs12 -export -out %s -in %s -name "%s"' %(certpfx, certpem, certpfx) )
        pexpect_try_expect(child, "Enter Export Password:", time=60)
        child.send('123qwe\n')
        pexpect_try_expect(child, "Verifying - Enter Export Password:", time=60)
        child.send('123qwe\n')
        pexpect_try_expect(child, pexpect.EOF, time=60)
        return certpfx


#MAIN

if len(sys.argv) == 1:
        print("Please, specify at least one FQDN.")
        sys.exit(1)

conftempl="openssl.cnf.template"
cahost="10.63.10.10"

sanlist=[]
for elem in sys.argv[1:]:
        sanlist.append(elem)

pk, pknosecure, crtreq = generateRequest(conftempl, sanlist)
cert = processCertificateRequest(crtreq, cahost)
certpfx = generatePFXCertificate(cert, pknosecure)


print("\n\nGenerated files:")
print("   %-40s %s" %("Encrypted private key:", pk))
print("   %-40s %s" %("Decrypted private key:", pknosecure))
print("   %-40s %s" %("Certificate request:", crtreq))
print("   %-40s %s" %("Issued certificate (PEM Format):", cert))
print("   %-40s %s" %("Issued certificate (PKCS#12/PFX Format):", certpfx))


Ссылки по теме

Creating a SubjectAltName (SAN/UCC) CSR: http://langui.sh/2009/02/27/creating-a-subjectaltname-sanucc-csr/
Setting Up a Certificate Authority in Windows: http://msdn.microsoft.com/en-us/library/ms755466(VS.85).aspx
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.