06 - Lokale Zertifizierungsstelle für SSL-Zertifikate mit OpenSSL

Zuletzt aktualisiert am 2. Februar 2024 11 Minuten

Sowas hätte ich gerne für meine Entwicklungsumgebung unter OS X, um für die Entwicklungsumgebung mit ihren verschiedenen *.local.web Domains SSL Zertifikate erstellen zu können. Ausserdem ist es mal spannend zu sehen wie so eine CA aufgebaut wird.

Als Zertifizierungsstelle (CA) zu fungieren bedeutet, mit kryptographischen Paaren aus privaten Schlüsseln und öffentlichen Zertifikaten umzugehen. Das allererste kryptographische Paar, das ich erstelle, ist das Root-Paar. Dieses besteht aus

  • dem Wurzelschlüssel - ca.key.pem
  • und dem Wurzelzertifikat - ca.cert.pem
  • Dieses Paar bildet die Identität der CA.

Normalerweise signiert die Root-CA Server- oder Client-Zertifikate nicht direkt. Die Root-CA wird immer nur dazu verwendet, eine oder mehrere Zwischen-CAs zu erstellen, denen die Root-CA vertraut, Zertifikate in ihrem Namen zu signieren. Das ermöglicht, den Root-Schlüssel offline und so weit wie möglich unbenutzt zu halten, da jede Kompromittierung des Root-Schlüssels katastrophal ist. Es ist bewährte Praxis, das Wurzelpaar in einer sicheren Umgebung zu erstellen. Idealerweise sollte das auf einem vollständig verschlüsselten Computer geschehen, der permanent vom Internet getrennt ist.

Falls openSSL noch nicht installiert ist sollte ich das jetzt machen:

brew install openssl
export PATH=$(brew --prefix openssl):$PATH

A: Arbeitsverzeichnis erstellen

…und darin eine Zertifizierungsstelle und ein Stammzertifikat erstellen.

mkdir -p /usr/local/opt/ca

cd /usr/local/opt/ca

mkdir -p certs crl newcerts private
chmod 700 private

mkdir --parents intermediate/certs
mkdir --parents intermediate/crl
mkdir --parents intermediate/newcerts
mkdir --parents intermediate/private
mkdir --parents intermediate/csr
  • certs - Hier liegt der Public Key der Zertifikatsstelle
  • crl -certificate revocation list -  Eine Liste, die die Ungültigkeit von Zertifikatenexternal link  beschreibt. Sie ermöglicht es, festzustellen, ob ein Zertifikat gesperrt oder widerrufen wurde und warum.
  • private - Hier liegt der private Schlüssel der CA
  • csr - Hier liegen die Anfragen?

Weitere Dateien, die OpenSSL zur Erstellung von Zertifikaten benötigt:

touch index.txt
echo 1000 > serial

touch intermediate/index.txt
echo 1000 > intermediate/serial
  • serial - Das File speichert die zuletzt vergebene eindeutige Seriennummer. Die Hexadezimalzahl muss mindestens zwei Stellen haben.
  • index.txt - Das File listet die von der CA ausgestellten Zertifikate.

Ein langes, komplexes Passwort in die Datei passwort.txt schreiben und speichern:

nano passwort.txt

Die Datei mit einem kürzeren und besser merkbaren Passwort verschlüsseln (TODO wird momentan nicht benutzt):

openssl enc -aes256 -salt -in passwort.txt -out passwort.enc

Die verschlüsselte Datei kontrollieren:

cat passwort.enc

Zur Kontrolle das lange Passwort nochmal entschlüsseln

openssl enc -aes256 -salt -d -in passwort.enc

und dann die nicht verschlüsselte Datei mit rm -f passwort.txt löschen.

B: OpenSSL-Konfiguration erstellen

Eine benutzerdefinierte OpenSSL-Konfiguration (openssl.cnf) erstellen, die für die Erstellung einer Zertifizierungsstelle (Root-SSL-Zertifikat) geeignet ist.

  1. Der Abschnitt [ ca ] ist obligatorisch. Hier weisen wir OpenSSL an, die Optionen aus der Sektion [ CA_default ] zu verwenden.
  2. Der Abschnitt [ CA_default ] enthält eine Reihe von Standardeinstellungen. Stelle sicher, dass du das Verzeichnis deklarierst, das du zuvor gewählt hast (/usr/local/opt/ca).
  3. Wende policy_strict für alle Root-CA-Signaturen an, da die Root-CA nur zur Erstellung von Zwischen-CAs verwendet wird.
  4. Wende policy_loose für alle Zwischen-CA-Signaturen an, da die Zwischen-CA Server- und Client-Zertifikate signiert, die von einer Vielzahl von Drittanbietern stammen können.
  5. Optionen aus dem Abschnitt [ req ] werden bei der Erstellung von Zertifikaten oder Zertifikatssignierungsanforderungen angewendet.
  6. Der Abschnitt [ req_distinguished_name ] deklariert die Informationen, die normalerweise in einer Zertifikatsunterzeichnungsanforderung erforderlich sind. Gib optional einige Standardwerte an.
  7. Die nächsten Abschnitte sind Erweiterungen, die beim Signieren von Zertifikaten angewendet werden können. Wenn du beispielsweise das Befehlszeilenargument -extensions v3_ca übergibst, werden die in [ v3_ca ] festgelegten Optionen angewendet. Ich wende die Erweiterung v3_ca an, wenn ich das Stammzertifikat erstelle.
  8. Ich wende die Erweiterung v3_ca_intermediate an, wenn ich das Zwischenzertifikat erstelle. pathlen:0 stellt sicher, dass es keine weiteren Zertifizierungsstellen unterhalb der Zwischen-CA geben kann.
  9. Ich wende die Erweiterung usr_cert an, wenn ich Client-Zertifikate signiere, wie sie z.B. für die Remote-Benutzerauthentifizierung verwendet werden.
  10. Ich wende die Erweiterung server_cert beim Signieren von Server-Zertifikaten, wie sie beispielsweise für Webserver verwendet werden, an.
  11. Die Erweiterung crl_ext wird bei der Erstellung von Zertifikatswiderrufslisten automatisch angewendet.
  12. Ich wende die ocsp - Erweiterung bei der Unterzeichnung des OCSP-Zertifikats (Online Certificate Status Protocol) an.
# OpenSSL root CA configuration file.
# Copy to `/usr/local/opt/ca/openssl.cnf`.

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /usr/local/opt/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  = 30

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

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        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only

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

# 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             = DE
stateOrProvinceName_default     = NRW
localityName_default            = Bergisch Gladbach
0.organizationName_default      = Carsten Nichte
organizationalUnitName_default  = Software-Entwicklung
emailAddress_default            = meine@mailadresse.de

[ 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
crlDistributionPoints = URI:http://www.local.web/intermediate.crl.pem

[ 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

[ san_env ]
subjectAltName=DNS:local.web,DNS:*.local.web

C: Zertifizierungsstelle schaffen

1.) Einen privaten Schlüssel erzeugen. Alle Zertifikate werden mit einem Passwort verschlüsselt. Ich setze hier mypassword ein. Bitte das lange Passwort von oben nehmen.

openssl genrsa -aes256 \
  -passout pass:mypassword \
  -out private/ca.key.pem 4096

Wichtig: Ein Backup des privaten Schlüssels machen (und gut sichern, zB. auf einem USB-Stick). Wenn der verloren geht, ist das SSL-Zertifikat wertlos.

2.) Einen Certificate Signing Request (CSR) erzeugen:

openssl req -config openssl.cnf \
  -key private/ca.key.pem -passin pass:mypassword \
  -new -x509 -days 7300 -sha256 -extensions v3_ca \
  -subj "/CN=OS-X Developer Root CA/O=Carsten Nichte/OU=Software-Entwicklung/C=CY/ST=NRW/L=Bergisch Gladbach" \
  -out certs/ca.cert.pem
  • Das CA-Zertifikat entspricht dem „Public Key“ der Zertifizierungsstelle.
  • -key - Der zum künftigen Zertifikat gehörende private Schlüssel
  • -out  - Der Name des künftigen Zertifikats
  • -x509 - Erstellt ein selbstsigniertes Zertifikat, anstelle einer gewöhnlichen Zertifikats-Anfrage
  • Das Zertifikat ist 7300 Tage - also 20 Jahre - gültig
  • -extensions v3_ca - Die Einstellungen aus der Datei openssl.cnf unter dem Abschnitt v3_ca a werden berücksichtigt.

D: Zwischenzertifizierungsstelle einrichten

Eine Konfigurationsdatei für die Zwischenstelle einrichten:

# OpenSSL root CA configuration file.
# Copy to `/usr/local/opt/ca/intermediate/openssl.cnf`.
#
# This file is based on the instructions found in https://jamielinux.com/notes/openssl-certificate-authority/index.html

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir = /usr/local/opt/ca/intermediate
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/intermediate.key.pem
certificate = $dir/certs/intermediate.cert.pem

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

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

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

[ 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 intermed# Al 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 = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only

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

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

[ req_distinguished_name ]
# See .
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 = DE
stateOrProvinceName_default = NRW
localityName_default = Bergisch Gladbach
0.organizationName_default = Carsten Nichte
organizationalUnitName_default = Software-Entwicklung
emailAddress_default = meine@mailadresse.de

[ 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

[ san_env ]
subjectAltName=DNS:local.web,DNS:*.local.web

1.) Den privaten Schlüssel für die Intermediate-CA erstellen. Wird später fürs elektronische Signieren der ausgestellten Zertifikate benutzt:

cd /usr/local/opt/ca

openssl genrsa -aes256 \
-passout pass:mypassword \
-out intermediate/private/intermediate.key.pem 4096

2.) Einen Certificate Signing Request (CSR) erzeugen:

openssl req -config intermediate/openssl.cnf -new -sha256 \
-key intermediate/private/intermediate.key.pem \
-passin pass:mypassword \
-subj "/CN=OS-X Developer Intermediate CA/O=Carsten Nichte/OU=Software-Entwicklung/C=CY/ST=NRW/L=Bergisch Gladbach" \
-out intermediate/csr/intermediate.csr.pem

Den erzeugten CSR würde ich jetzt normalerweise an eine offizielle Zertifizierungsstelle übermitteln bzw. hochladen. Ich mache das aber selbst, und nutze wie oben meine eigene CA dafür:

3.) CSR für Intermediate-CA-Zertifikat mit Root-CA signieren.

Ich unterzeichne den CSR mit der eigenen Root-CA . Das Zertifikat ist 3650 Tage - also rund 10 Jahren gültig.

openssl ca -config openssl.cnf -extensions v3_intermediate_ca -batch \
-passin pass:mypassword \
-days 3650 -notext -md sha256 \
-in intermediate/csr/intermediate.csr.pem \
-out intermediate/certs/intermediate.cert.pem

In der index.txt im Root-CA-Verzeichnis nachschauen, und das Zertifikat ansehen:

openssl x509 -noout -text -in intermediate/certs/intermediate.cert.pem

4.) Zertifikatskette erstellen.

Die Zertifikatskette / Certificate-Chain besteht aus den aneinander gehängten einzelnen Zertifikaten, das unterste der Hierarchie steht zuerst in der Datei, das oberste - also die Root-CA als letztes.

cat intermediate/certs/intermediate.cert.pem \
certs/ca.cert.pem > intermediate/certs/ca-chain.cert.pem

5.) Mit der Root-CA verifizieren (todo, verify-a-certificate-chain-using-openssl-verifyexternal link )

openssl verify -CAfile intermediate/certs/ca-chain.cert.pem

E: SSL-Zertifikat für den Entwicklungsserver erstellen

Als letztes erstellen wir das SSL-Zertifikat für den Entwicklungsserver, und kopieren alles nach /usr/local/etc/httpd/ssl.

cd /usr/local/opt/ca

1.) Den privaten Schlüssel für *.local.web erzeugen:

openssl genrsa -aes256 \
  -passout pass:mypassword \
  -out intermediate/private/local.web.key.pem 2048

2.) Einen Certificate Signing Request (CSR) für *.local.web erzeugen:

openssl req -config /usr/local/opt/ca/intermediate/openssl.cnf \
  -key intermediate/private/local.web.key.pem \
  -extensions san_env \
  -passin pass:mypassword \
  -subj "//CN=*.local.web/O=Carsten Nichte/OU=Software-Entwicklung/C=CY/ST=NRW/L=Bergisch Gladbach" \
  -new -sha256 -out intermediate/csr/local.web.csr.pem

Den erzeugten CSR würde ich jetzt normalerweise an eine offizielle Zertifizierungsstelle übermitteln bzw. hochladen. Ich mache das aber selbst, und nutze meine eigene CA dafür:

3.) Den CSR mit dem Intermediate-CA-Zertifikat signieren

openssl ca -config intermediate/openssl.cnf -batch \
  -extensions server_cert -extensions san_env \
  -days 1835 -notext -md sha256 \
  -in intermediate/csr/local.web.csr.pem \
  -passin pass:mypassword \
  -out intermediate/certs/local.web.cert.pem
  • 1835 Tage gültig - also ca. 5 Jahre

4.) und alles kopieren…

mkdir /usr/local/etc/httpd/ssl

Die Zertifikatskette, …

cp intermediate/certs/ca-chain.cert.pem /usr/local/etc/httpd/ssl

…den Schlüssel, …

openssl rsa -in intermediate/private/local.web.key.pem \
  -out /usr/local/etc/httpd/ssl/local.web.key \
  -passin pass:mypassword

Erzeugt aus der mit Passwort geschützten Schlüsseldatei local.web.key.pem eine Schlüsseldatei ohne Passwortschutz local.web.key. Die ungeschützte Schlüsseldatei muss vor unbefugtem Zugriff geschützt werden, zB. über Dateizugriffsrechte.

…und das Zertifikat…

cp intermediate/certs/local.web.cert.pem /usr/local/etc/httpd/ssl/local.web.crt

Bis hierhin hat das super funktioniert. Jetzt will ich das Ganze natürlich noch im Apache aktivieren und nutzen.

F: SSL-Unterstützung im Apache aktivieren

codium /usr/local/etc/httpd/httpd.conf

Finde Zeile

Listen 80

Eine weitere Zeile hinzufügen, damit Apache auch auf dem Standard-HTTPS-Port, lauscht (443).

Listen 80
Listen 443

HTTPS-Unterstützung im Apache aktivieren. Die folgenden Zeilen suchen und das # davor entfernen.

#LoadModule ssl_module lib/httpd/modules/mod_ssl.so
#LoadModule http2_module lib/httpd/modules/mod_http2.so

Wird fortgesetzt….