Sinatra / RAILS / Whatever - Login mit SSL Client Zertifikat

04. Dezember 2019 | Aktualisiert 05. Dezember 2019

Ich beschreibe hier anhand meiner Sinatra-Applikation (also diejenige, die diesen Blog antreibt), wie man mittels Apache und OpenSSL einen Benutzer via Client Zertifikat authentifiziert. Persönlich benutze ich Client SSL Zertifikate schon ca. 4 Jahre, habe das aber nie dokumentiert.

  • Das was ich hier beschreibe ist nicht an ein aktuelles Archlinux gebunden. Vielleicht nicht mal an ein unixartiges OS...
  • Du kannst die Skripte höchstwarscheinlich 1:1 auch auf einem 5 Jahre alten Debian benutzen, sofern OpenSSL die entsprechenden Optionen kennt.
  • Bezüglich der Abfrage des Accounts, welcher sich da gerade via Zertifikat anmelden will, besteht bez. RAILS oder Sinatra keinerlei Unterschied. Und ich behaupte, das man die Server-Umgebungsvariable REMOTE_USER auch in anderen Frameworks ganz leicht auslesen kann.
  • Dein Webserver kann natürlich (Open)SSL, gell?
  • Alle referenzierten Dateien befinden sich im gleichen Verzeichnis
  • Keines der Skripte nimmt (derzeit) Parameter an. Was aber unbedingt notwendig wäre!
  • Fehlerbedingungen werden ignoriert. Falls irgendwo im Skript ein Fehler passiert, wird das Ergebnis unvorhersehbar sein!
  • In der openssl.cnf habe ich für mich sinnvolle Werte vorbelegt, so das ich meisst nur noch ENTER zu drücken brauche. Die Datei muss nicht vorhanden sein!

Let's begin. Ich gehe mal davon aus, das deine Webseite läuft, ok?

Wir fangen mal auf dem Server an.

  • Hier steht und fällt alles mit der Anforderung, das du root-Zugang hast. Wenn nicht, kannst du hier aufhören zu lesen.

OK, wir fangen mit dem Apache an. Ich betreibe keinen nginx aber prinzipiell sollte das übertragbar sein. Erweitere deine vhost.conf (wie auch immer die bei dir heisst) mit den entsprechenden Anpassungen. Siehe nachfolgendes Beispiel:

<VirtualHost x.x.x.x:443>

    # other settings

    # Client CERT's
    # siehe https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html
    SSLCACertificatePath /path/to/certs/dir/
    SSLCACertificateFile /path/to/certs/dir/ca-certificate.pem
    SSLVerifyClient none

    # match with app route!
    <Location "/login/by_cert">
        SSLVerifyClient optional
        SSLOptions +ExportCertData
        SSLVerifyDepth 1
        # Der Wert des CN, den man beim Erstellen des Zertifikates angegeben hat
        # steht dann im REMOTE_USER Wert und kann gegen die DB abgefragt werden.
        SSLUserName SSL_CLIENT_S_DN_CN
    </Location>

    # other settings

</VirtualHost>

Damit ist der Apache fertig. Wichtig ist der Wert von SSLCACertificateFile (anzupassen) und der Inhalt des Location-Tags (eher weniger anzupassen)

So, falls ich nichts anderes behaupte, finden alle weiteren Aktionen auf dem Client statt

 

Kommen wir zu den Skripten, die das Zertifikat erstellen

 

env.sh

Umgebungsvariable setzen

#!/bin/sh
## filename: env.sh
##
## Dieses File wird in den gen_* Skripten benutzt um
## Dateinamen und Verzeichnisse vorzubelegen. Und natürlich
## kann und soll das angepasst werden - wenn notwendig!
##
export STORE_BASE=./sslstore
export OPENSSL_CONF=./openssl.cnf
export PASSWD=xxx # warum muss da eigentlich ein Kennwort verwendet werden?
export CA_KEY=${STORE_BASE}/private/cakey.pem
export SERVER_CERT=${STORE_BASE}/CA/cacert.pem
export CERTS_DIR=${STORE_BASE}/certs
export DAYS=3650 # das hält lange aus ;)
export BITS=4096 # und sicher(er) ist es womöglich auch noch ;)

# Lege Verzeichnisse an wenn notwendig
[[ -d ${STORE_BASE} ]] || mkdir ${STORE_BASE}
[[ -d ${STORE_BASE}/CA ]] || mkdir ${STORE_BASE}/CA
[[ -d ${STORE_BASE}/private ]] || mkdir ${STORE_BASE}/private
[[ -d ${CERTS_DIR} ]] || mkdir ${CERTS_DIR}

 

gen_ca.sh

erzeugt die CA (Certificate Authority) und den Private Key

#!/bin/sh
## filename: gen_ca.sh

. ./env.sh

TMP_FILE=ca.pass.key
openssl genrsa -aes256 -passout pass:${PASSWD} -out ${TMP_FILE} ${BITS}
openssl rsa -passin pass:${PASSWD} -in ${TMP_FILE} -out ${CA_KEY}
rm ${TMP_FILE}

openssl req -new -x509 -days ${DAYS} -key ${CA_KEY} -out ${SERVER_CERT}

An der Stelle kann man bereits die erzeugte Datei cacert.pem (oder wie immer Du sie genannt hast) auf den Server in das oben angegebene Verzeichnis kopieren. Stelle dabei sicher, das der Apache/httpd Prozess die Datei lesen kann.

 

gen_client.sh

erzeugt das Zertifikat für einen beliebigen User basierend auf gen_ca.sh. OpenSSL fragt hier den Wert für CN ab. Dieser Wert landet später, wenn der Webbrowser die URL aufruft und das Client-Zertifikat verifiziert werden kann, in der Umgebungsvarible REMOTE_USER (hatte ich schon erwähnt, gell?)

#!/bin/sh
## filename: gen_client.sh

. ./env.sh

USER=tfl # make it parameterizeable

CLIENT_SERIAL=01 # make it parameterizeable too!
CLIENT_ID_TMP="${STORE_BASE}/${USER}.tmp"
CLIENT_CERT_BASE="${CERTS_DIR}/${USER}"

CLIENT_ID_KEY="${CLIENT_CERT_BASE}/${USER}.key"
CLIENT_ID_CSR="${CLIENT_CERT_BASE}/${USER}.csr"
CLIENT_ID_PEM="${CLIENT_CERT_BASE}/${USER}.pem"
CLIENT_ID_FULL_PEM="${CLIENT_CERT_BASE}/${USER}.full.pem"

# Create output directory for user certificate
[[ -d ${STORE_BASE}/certs/${USER} ]] || mkdir ${STORE_BASE}/certs/${USER}

openssl genrsa -aes256 -passout pass:$PASSWD -out ${CLIENT_ID_TMP} ${BITS}
openssl rsa -passin pass:$PASSWD -in ${CLIENT_ID_TMP} -out ${CLIENT_ID_KEY}
rm ${CLIENT_ID_TMP}

# generate the CSR
openssl req -new -key ${CLIENT_ID_KEY} -out ${CLIENT_ID_CSR}

# issue this certificate
openssl x509 -req -days ${DAYS} -in ${CLIENT_ID_CSR} -CA ${SERVER_CERT} -CAkey ${CA_KEY} -set_serial ${CLIENT_SERIAL} -out ${CLIENT_ID_PEM}

# Bundle the private key & cert for end-user client use
cat ${CLIENT_ID_KEY} ${CLIENT_ID_PEM} ${SERVER_CERT} > ${CLIENT_ID_FULL_PEM}

# Bundle client key into a PFX file
openssl pkcs12 -export -out ${CLIENT_ID_FULL_PEM}.pfx -inkey ${CLIENT_ID_KEY} -in ${CLIENT_ID_PEM} -certfile ${SERVER_CERT}

 

Jetzt gibt es im SSLSTORE eine .pfx Datei. Diese muss man, unter Angabe des Kennwortes, das man im vorherigen Schritt hat eingeben müssen, in seinen Browser importieren.

 

Anpassung der Web-Applikation

Im letzten Schritt muss man seiner RAILS/Sinatra/Whatever-Webapp die Route beibringen, in der der Wert von REMOTE_USER abgefragt wird. Zur Erinnerung: sie muss identisch zum Location Eintrag in der httpd.conf sein. Dann muss es natürlich im View eine Möglichkeit geben, auf den entsprechenden Link zu klicken.

 

Mit dem ermittelten Wert kann man dann seine Datenbank abfragen und versuchen, den User zu authentifizieren. Eigentlich ganz einfach.

##
## Login by Client Certificate
##
get '/login/by_cert' do
	user = User.find_by_cn(request.env['REMOTE_USER'])
	if user
		session['user_id'] = user.id
		flash[:success] = to_html "User <em>#{user.cn}</em> logged in by SSL Client certificate"
		redirect '/'
	else
		flash.now[:warning] = 'Invalid Certificate'
		erb :'session/new'
	end
end

 

Anmerkung #1: Benutze die vorhandene Emailadresse zum authentifizieren

 

Anstatt wie ich eine neue Spalte zur Benutzertabelle hinzuzufügen, kann man natürlich auch eine vorhandene Spalte/Eigenschaft benutzen, die denjenigen Wert enthält, den man auch via SSL-Zertifikat abfragen kann. Und da bietet sich natürlich die Mailadresse an. Und in der Tat benutzen sehr viele Anwendungen im Internet die Mailadaresse für die Anmeldung eines Benutzers anstatt eines Benutzernamens.

 

Um das zu erreichen, muss man nicht wirklich viel an meinem Beispiel ändern:

  1. Die Apache-Konfiguration im Location-Abschnitt muss auf SSLUserName SSL_CLIENT_S_DN_EMAIL geändert werden.
  2. Die Web-Applikation muss natürlich die entsprechend vorhandene Spalte abfragen: User.find_by_email(request.env['REMOTE_USER'])
  3. Man muss natürlich beim generieren des Client-Zertifikates die richtige Emailadresse eingeben, sobald OpenSSL danach fragt.

Die Details dazu bzw. wie und warum man das so machen kann, verrät wieder die Apache Dokumentation