PKI OpenSSL (pt 1)

PKI baseada em OpenSSL (parte 1)

Fontes:
https://openssl-ca.readthedocs.io/en/latest/index.html
https://www.golinuxcloud.com/tutorial-pki-certificates-authority-ocsp/
https://social.technet.microsoft.com/wiki/contents/articles/2900.offline-root-certification-authority-ca.aspx
https://pki-tutorial.readthedocs.io/en/latest/index.html

Root CA airgapped

É suposto o sistema que contém a RootCA ser airgapped, ter acesso controlado, e ser ligado apenas para emissão de novas CAs e revogação de CAs comprometidas. Neste exemplo, podemos colocar tudo no mesmo servidor mas deve-se tentar separar o mais possível a estrutura da RootCA das outras CAs.
O primeiro passo é criar uma Root Certification Authority (RootCA), que só vai criar outras CAs.
Mudar para o utilizador root

1
2
3
4
5
6
7
mkdir /root/ca && cd $_
mkdir /root/ca/{certs,crl,newcerts,private}
chmod 700 /root/ca/private
touch /root/ca/index.txt
echo 1000 > /root/ca/serial
echo 1000 > /root/ca/crlnumber
cat > /root/ca/openssl.cnf

/root/ca/openssl.cnf

# OpenSSL root CA configuration file.
# Copy to '/root/ca/openssl.cnf.
[ ca ]
# 'man ca'
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /root/ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# The root key and root certificate.
private_key       = $dir/private/ca.key.pem
certificate       = $dir/certs/ca.cert.pem

# For certificate revocation lists.
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 385

# SHA-1 is deprecated, so use SHA-2 instead.
default_md        = sha512

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of 'man ca'.
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the 'ca' man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
# Options for the 'req' tool ('man req').
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md          = sha512

# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = PT
stateOrProvinceName_default     = Lisboa
localityName_default            = Lisboa
0.organizationName_default      = TiagoJoaoSilva
organizationalUnitName_default  = TJS
emailAddress_default            = bofh@tjs.lan

[ v3_ca ]
# Extensions for a typical CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
# Extensions for client certificates ('man x509v3_config').
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates ('man x509v3_config').
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs ('man x509v3_config').
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates ('man ocsp').
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning


Criamos uma palavra-passe complexa:

1
2
3
openssl rand -base64 12 > /root/ca/private/ca.key.pass
chmod 400 /root/ca/private/ca.key.pass
cat /root/ca/private/ca.key.pass


ayUiXnIjZFRh


Criamos a chave indicando a palavra-passe complexa:

1
2
openssl genrsa -aes256 -passout file:/root/ca/private/ca.key.pass \
-out /root/ca/private/ca.key.pem 8192


Generating RSA private key, 8192 bit long modulus (2 primes)
......................................................+++
..+++
e is 65537 (0x010001)


Criamos e auto-assinamos o certificado da RootCA:

1
2
3
4
5
6
chmod 400 /root/ca/private/ca.key.pem
openssl req -config /root/ca/openssl.cnf \
    -passin file:/root/ca/private/ca.key.pass \
    -key /root/ca/private/ca.key.pem \
    -new -x509 -days 7300 -sha512 -extensions v3_ca \
    -out /root/ca/certs/ca.cert.pem

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [PT]:
State or Province Name [Lisboa]:
Locality Name [Lisboa]:
Organization Name [TiagoJoaoSilva]:
Organizational Unit Name [TJS]:
Common Name []:ca-tjs-1
Email Address [bofh@tjs.lan]:

1
chmod 444 /root/ca/certs/ca.cert.pem

Podemos examinar o conteúdo do certificado:

1
openssl x509 -noout -text -in /root/ca/certs/ca.cert.pem

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            25:92:4d:14:da:6f:9f:8b:4b:49:55:fc:68:be:9e:c7:57:24:cd:70
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Validity
            Not Before: Aug 12 19:38:30 2021 GMT
            Not After : Aug  7 19:38:30 2041 GMT
        Subject: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (8192 bit)
                Modulus: 
[...]
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0
            X509v3 Authority Key Identifier:
                keyid:01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0

            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
    Signature Algorithm: sha512WithRSAEncryption
 [...]

Revogação de certificados

Antes de irmos mais longe, acho que temos de falar na revogação de certificados, ao contrário de todos os outros tutoriais da Internet que deixam esse assunto para o fim, se chegam sequer a falar nele.
Se não o fizermos agora, será demasiado tarde para ser incluído em qualquer certificado criado pela RootCA.

Como revogar um certificado

1
2
3
4
5
6
stamp=$(date +%F_%T)
mv /root/ca/inter-ca/certs/example-ca.cert.pem{,-revoked_$stamp}
openssl ca -config /root/ca/openssl.cnf \
    -passin file:/root/ca/private/ca.key.pass \
    -revoke /root/ca/inter-ca/certs/example-ca.cert.pem-revoked_$stamp \
    -crl_reason unspecified

-crl_reason também pode ter os valores keyCompromise, CACompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, ou (só em Delta-CRLs) removeFromCRL
-crl_hold coloca automaticamente a reason como certificateHold mas está fora do âmbito deste documento
-crl_compromise AAAAMMDDHHMMSSZ coloca a reason como
keyCompromise na data assinalada.
-crl_CA_compromise AAAAMMDDHHMMSSZ faz o mesmo, mas a reason é CACompromise

Precisamos de configurar um mecanismo para a RootCA disseminar a informação dos certificados revogados, e isso precisa de ser feito antes de se criar qualquer certificado subsequente, acrescentando informações ao openssl.cnf da RootCA antes de criar qualquer CA secundária.

Distribuição de CRL

O método clássico é a distribuição directa das listas CRL, que pode ser feita por vários meios, como um servidor HTTP, uma directoria LDAP ou AD, etc.
Pode existir ainda uma complicação adicional, porque para evitar a transmissão constante de ficheiros grandes podem-se utilizar Delta-CRLs apenas com as alterações ao CRL; no entanto, apesar de esta ser uma possibilidade em Windows ou em outros pacotes de PKI, não está implementada no OpenSSL.
Estas listas costumam ser actualizadas (porque expiram e todos os certificados dessa CA deixam de ser aceites) no máximo de 30 em 30 dias, mas a lista de CRLs da RootCA não deve expirar (ou expirar apenas anualmente quando for altura de actualizar o sistema que contém a RootCA).

[CA_default]
default_crl_days  = 385

Nessa altura, refresca-se o CRL e (no caso de o sistema da RootCA ser airgapped), extrair o CRL (em papel, QR Code, CD-R ou outro meio seguro) e levá-lo para o sistema de onde vai ser distribuído.
O formato de distribuição destas listas é DER (codificação binária), mas o ficheiro tem de ter a extensão .CRL (RFC 2585).

O outro método, mais recomendado nos dias de hoje, é a utilização de um OCSP Responder para responder a consultas sobre a validade de um certificado, que é uma operação rápida que não necessita de muita largura de banda, permite que a disseminação da revogação seja assinada com uma chave da CA-mãe, e que a resposta fique em cache nos clientes (através de OCSP Stapling).
Como o CRL da RootCA vai conter muito poucos certificados (a RootCA só vai emitir outras CAs, chamadas IntermediateCAs, SubordinateCAs ou SigningCAs, que serão poucas), vamos configurar uma emissão manual de CRLs para a RootCA.

Começamos por colocar a informação do local de onde será servido o CRL na secção [ v3_intermediate_ca ] do /root/ca/openssl.cnf, para ser incluída nos certificados das CAs emitidas pela RootCA. Vamos utilizar um servidor HTTP na porta 7788:

[ v3_intermediate_ca ]
crlDistributionPoints = URI:http://server.tjs.lan:7788/root-ca.crl

Criamos o CRL (https://blog.didierstevens.com/2013/05/08/howto-make-your-own-cert-and-revocation-list-with-openssl/)

1
2
3
4
openssl ca -config /root/ca/openssl.cnf -gencrl \
    -passin file:/root/ca/private/ca.key.pass \
    -keyfile /root/ca/private/ca.key.pem -cert /root/ca/certs/ca.cert.pem \
    -out /root/ca/crl/ca.crl.pem

Verificamos o conteúdo do CRL:

1
openssl crl -in /root/ca/crl/ca.crl.pem -noout -text


Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Last Update: Aug 13 23:46:07 2021 GMT
        Next Update: Sep  2 23:46:07 2022 GMT
        CRL extensions:
            X509v3 Authority Key Identifier:
                keyid:01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0

            X509v3 CRL Number:
                4097
Revoked Certificates:
    Serial Number: 1001
        Revocation Date: Aug 13 22:11:25 2021 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code:
                Unspecified
    Serial Number: 1002
        Revocation Date: Aug 13 23:41:51 2021 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code:
                Superseded
    Signature Algorithm: sha512WithRSAEncryption 
    [...]


E temos de converter o PEM (Base64) para formato DER:

1
2
# openssl crl -inform PEM -in /root/ca/crl/ca.crl.pem \
-outform DER -out /root/ca/crl/root-ca.crl

Sistema ligado à rede

Como a RootCA deve ser airgapped e offline, não é no sistema da RootCA que se deve configurar a distribuição de CRLs, porque esse sistema tem de estar acessível na rede.
Configuramos um novo sistema Debian 11 e recriamos a pasta que contém o CRL da RootCA:

1
mkdir -p /root/ca/crl

Fazemos lá chegar o ficheiro root-ca.crl de alguma maneira segura (gravar um CD-R, impressão e scanning com OCR, etc.) e colocámo-lo em /root/ca/crl

Instalar e configurar o servidor web do CRL

Vamos usar um servidor muito básico, o lighttpd

1
apt install lighttpd -y

De acordo com o wiki do Debian sobre o Lighttpd a instalação por defeito configura o serviço, agarra a porta 80/TCP, corre com o utilizador www-data, configura a webroot para ser /var/www/html, e os ficheiros default são index.php ou index.html, se bem que venha configurado para não correr ficheiros .php.
Vamos configurar outra porta (7788), a webroot será outra para não interferir com a default que é usada por outros servidores web, e desligar o desvio automático para index.html
Além disso, o que pode ser importante, a configuração de tipos MIME foi pré-feita com um script Perl, que se calhar vamos ter de editar ou refazer porque os ficheiros CRL têm de ser servidos por HTTP com um tipo MIME específico, application/pkix-crl
(https://pki-tutorial.readthedocs.io/en/latest/mime.html)

1
cp /etc/lighttpd/lighttpd.conf{,.orig}

Mudar as linhas:

 1 server.modules = (
 2 #   "mod_indexfile",
 3 #   "mod_access",
 4 #   "mod_alias",
 5 #   "mod_redirect",
 6 )
 8 server.document-root        = "/var/www/crl"
10 server.errorlog             = "/var/log/lighttpd/crl-error.log"
14 server.port                 = 7788
41 #index-file.names            = ( "index.php", "index.html" )

Criar a nova pasta webroot:

1
mkdir /var/www/crl

Mover para lá o index.html deixado pelo pacote lighttpd e desactivá-lo:

1
2
mv /var/www/html/index.lighttpd.html
/var/www/crl/index.lighttpd.html.disabled

Colocar o CRL em formato DER dentro da webroot:

1
2
3
cp /root/ca/crl/root-ca.crl /var/www/crl/root-ca.crl
chown www-data:www-data /var/www/crl/root-ca.crl
chmod 400 /var/www/crl/root-ca.crl

Para configurar os tipos MIME, o lighttpd.conf chama um script que cria uma configuração MIME a partir dos tipos definidos em /etc/mime.types; será que este último já tem uma definição para o tipo MIME do CRL?

1
grep pkix-crl /etc/mime.types


application/pkix-crl crl


Portanto, a linha 47 do lighttpd.conf vai criar uma lista de tipos MIME que inclui o nosso tipo CRL de cada vez que o serviço lighttpd for iniciado.

47 include_shell "/usr/share/lighttpd/create-mime.conf.pl"

Confirmamos a sintaxe do ficheiro lighttpd.conf:

1
lighttpd -tt -f /etc/lighttpd/lighttpd.conf


2021-08-14 02:26:38: configfile.c.1142) WARNING: unknown config-key:
url.access-deny (ignored)


Este erro não é importante, por isso:

1
2
systemctl restart lighttpd
systemctl status lighttpd

● lighttpd.service - Lighttpd Daemon
Loaded: loaded (/lib/systemd/system/lighttpd.service; enabled; vendor preset: enabled)
Active: active (running)

1
systemctl enable lighttpd


Synchronizing state of lighttpd.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable lighttpd


Abrimos a porta 7788 na firewall:

1
ufw allow 7788

E testamos o acesso ao ficheiro:

1
wget localhost:7788/root-ca.crl


Resolving localhost (localhost)... ::1, 127.0.0.1
Connecting to localhost (localhost)|::1|:7788... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1359 (1.3K) [application/pkix-crl]
Saving to: ‘root-ca.crl’

root-ca.crl                   100%[=================================================>]   1.33K  --.-KB/s    in 0s

2021-08-14 02:33:18 (168 MB/s) - ‘root-ca.crl’ saved [1359/1359] 


Foi estabelecida ligação e o ficheiro é servido com o tipo MIME correcto.

Mas infelizmente, só se devem usar CRLs quando temos a certeza que os clientes suportam este formato (e nesse aspecto, quem se porta melhor é a Microsoft); por exemplo, é impossível revogar certificados de Webserver por distribuição de CRLs porque apenas o Internet Explorer tem código para descarregar e analisar um CRL (Firefox e Chrome não querem saber) https://news.netcraft.com/archives/2013/05/13/how-certificate-revocation-doesnt-work-in-practice.html .
Por isso é mais seguro configurar o mecanismo alternativo, que é um OCSP Responder, também chamado AIA devido à directiva que o configura ( AuthorityInfoAccess).

Configuração do CNF com o endereço do OCSP Responder

Começamos por colocar no openssl.cnf da RootCA a seguinte directiva, na secção [ intermediate_ca ]

authorityInfoAccess = OCSP;URI:http://ocsp.tjs.lan:7899

A porta TCP do OCSP pode ser qualquer uma, sendo usual colocá-lo numa porta de tráfego HTTP pouco usada (como a 8008). Escolhi a porta 7899 porque depois de analisada a lista de portas da IANA, as portas 7888-7899/TCP não estão atribuídas, podendo ser usadas para este fim. Também se poderia desligar o lighttpd e reutilizar a porta 7788.
O protocolo é HTTP e não HTTPS porque como se verificaria o certificado SSL do OCSP? (https://social.technet.microsoft.com/Forums/office/en-US/c65e1784-39be-4732-a135-bfba7446ad05/should-the-ocsp-responder-service-be-running-http-80-or-https-443-)
A configuração de um OCSP Responder será documentada no segmento seguinte, que será a configuração da IntermediateCA; deixamos já a informação no .CNF da RootCA caso mais tarde se deseje implementar um OCSP Responder para ela também.
Note-se: se estas informações não forem configuradas agora, só poderão ser introduzidas na PKI (e passadas a certificados emitidos pela RootCA) revogando e re-emitindo todos os certificados já emitidos; por isso a estrutura de uma PKI deve ser ponderada antes de ser implementada.

Podemos passar à configuração da IntermediateCA.