Facebook
Twitter
Google+
Kommentare
0

Client-Zertifikate als sicherer Login-Ersatz?

Wer auf Sicherheit achtet und seinen Webseitenbesuchern etwas Privatsphäre spendieren möchte installiert ein SSL-Zertifikat auf dem eigenen Webserver. Damit ist es Besuchern möglich verschlüsselt mit dem Webserver zu kommunizieren und ein eventuell vorhandener Mithörer im offenen WLAN guckt dumm aus der Wäsche. Spätestens wenn es um Login-Daten oder andere persönliche Informationen geht sollte HTTPS eigentlich mittlerweile Standard sein, aber auch für normale Seiten lohnt es sich, denn bereits eine URL verrät einiges über eine Person, auch wenn die Seite eigentlich nichts geheimes enthält.

Hier soll es nicht direkt um die Ausstattung des Webservers mit einem Serverzertifikat gehen. Ein Serverzertifikat ist dafür geeignet eine verschlüsselte Kommunikation ermöglichen und einem Besucher die Sicherheit geben dass er auf dem richtigen Server gelandet ist. Ein Client-Zertifikat hingegen dient zur Authentifizierung eines Besuchers. Wenn ein Besucher ein bestimmtes Zertifikat besitzt und das durch eine kleine Abfrage beweisen kann ist das gleichzusetzen mit einem Login via Username und Passwort. Doch Achtung: Auch ein Zertifikat auf dem PC eines Users kann von Trojanern und ähnlichem gestohlen werden, es ist nicht der Weisheit letzter Schluss. Lösungen die zusätzlich noch ein Hardwaregerät (wie Handy oder Token (Yubikey!)) benötigen sind eventuell zu bevorzugen. Außerdem kann die Verwendung problematisch sein wenn man sich beispielsweise zuhause einen Rechner teilt und keine getrennten Browser-Profile hat. Doch der Reihe nach.

Basis für den Einsatz von Client-Zertifikaten ist eine HTTPS-Verschlüsselung. Zuerst müssen wir also sicherstellen dass unser Webserver unter dem Port 443 erreichbar ist und SSL spricht. Dazu generieren wir uns schnell ein selbst signiertes Zertifikat. Natürlich kann auch ein richtiges Zertifikat genutzt werden, dazu benötigt man nur den Certificate Signing Request (CSR) und schickt diesen zum entsprechenden Anbieter.

openssl req -new > server.cert.csr
openssl rsa -in privkey.pem -out server.cert.key
openssl x509 -in server.cert.csr -out server.cert.crt  -req -signkey server.cert.key -days 365
sudo mkdir /etc/apache2/ssl
sudo mv server.cert.* /etc/apache2/ssl
sudo a2enmod ssl

Im entsprechenden VHost des Webservers dann SSL aktivieren (hier am Beispiel des Apache):

<VirtualHost *:443>
    ServerName phpgangstatest.de
    SSLEngine on
    SSLCertificateKeyFile /etc/apache2/ssl/server.cert.key
    SSLCertificateFile /etc/apache2/ssl/server.cert.crt
    ...
</VirtualHost>

Falls noch nicht vorhanden muss noch eine “Listen 443″ Zeile in die /etc/apache2/ports.conf eingetragen werden. Bei mir war das bereits der Fall.

Nach einem Neustart des Apache ist die Seite per https:// erreichbar.

sudo service apache2 restart

Nun haben wir die Vorbedingung erfüllt und können uns den Client-Zertifikaten widmen. Standardmäßig verlangt ein Webserver kein Client-Zertifikat, das müssen wir also ändern.

Client-Zertifikate können natürlich von richtigen CAs signiert werden, in unserem Fall reicht aber ein selbst signiertes Zertifikat unserer eigenen CA. Dazu kann man sich ein eigenes Root-CA-Zertifikat erstellen das nur dafür gedacht ist Client-Zertifikate zu signieren, oder man tut dies mit dem bereits existierenden Server-Zertifikat das der Webserver ja bereits hat und dem er vertraut. Wenn man das ganze nicht nur zum Test macht sollte man darauf achten welche Angaben man macht. Dazu sollte auch die Datei /etc/ssl/openssl.cnf bearbeitet werden, dann kann man sich das unten stehende “demoCA” ersparen wenn man das “dir” richtig einstellt etc.

Wir generieren nun also unser erstes Client-Zertifikat:

mkdir -p demoCA
touch demoCA/index.txt
echo 1001 > demoCA/serial
genrsa -des3 -out michael.key
openssl req -new -key michael.key -out michael.req
openssl ca -cert /etc/apache2/ssl/server.cert.crt -keyfile /etc/apache2/ssl/server.cert.key -out michael.crt -in michael.req

Wir haben damit ein Schlüsselpaar für den Benutzer michael erzeugt und es mit dem CA-Zertifikat (hier gleich dem selbst erstellten Server-Zertifikat) signiert.

Damit die meisten Browser das Client-Zertifikat installieren können wandeln wir es noch um in das PKCS#12 Format:

openssl pkcs12 -export -inkey michael.key -name "Michael" -in michael.crt -certfile /etc/apache2/ssl/server.cert.crt -out michael.p12

Bei diesem Export werden wir nach einem Export-Passwort gefragt. Dieses Export-Passwort geben wir dann dem entsprechenden Benutzer zusammen mit der .p12 Datei.

Nun kommt als nächstes der Server. Man kann das Vorhandensein eines Client-Zertifikats optional oder verpflichtend fordern. Je nachdem welche Applikation man schützen möchte gibt man den Benutzern noch die Möglichkeit sich via Username+Passwort einzuloggen, oder eben nicht.

<VirtualHost *:443>
    ...
    SSLCACertificateFile /etc/apache2/ssl/server.cert.crt
    SSLVerifyClient require
    SSLVerifyDepth 1
    SSLOptions +StdEnvVars
</VirtualHost>

Die Client-Zertifikate müssen also abgeleitet sein vom SSLCACertificateFile. SSLVerifyClient kann alternativ auch auf “optional” gestellt werden. Die Einstellung SSLVerifyDepth definiert die maximal Tiefe der Zertifikatshierarchie. In diesem Fall muss das Client-Zertifikat also direkt vom CA-Zertifikat signiert sein. Zu den SSLOptions kann zusätzlich noch +ExportCertData hinzugefügt werden, dann wird das komplette Client Zertifikat auch in PHP zur Verfügung stehen. So erhält PHP also Zugriff auf die Werte des Client Zertifikats und kann daran später erkennen wer gerade die Webseite betreten hat.

Nach einem Neustart des Webservers fragt der Webserver nach einem Client-Zertifikat. Der Firefox beispielsweise präsentiert im require-Fall diese unschöne Fehlermeldung falls man kein passendes Client-Zertifikat installiert hat:

Ein Fehler ist während einer Verbindung mit phpgangstatest.de aufgetreten.
Die SSL-Gegenstelle konnte keinen akzeptablen Satz an Sicherheitsparametern aushandeln.
(Fehlercode: ssl_error_handshake_failure_alert)

Im Chrome ist die Fehlermeldung etwas verständlicher:

SSL-Verbindungsfehler
Es kann keine sichere Verbindung zum Server hergestellt werden. Möglicherweise liegt ein
Problem mit dem Server vor oder es ist ein Client-Authentifizierungszertifikat erforderlich,
das Sie nicht haben.
Fehler 107 (net::ERR_SSL_PROTOCOL_ERROR): SSL-Protokollfehler

Damit wir auf die Seite zugreifen können importieren wir nun also das Client-Zertifikat michael.p12 . Im Firefox geht das unter

Einstellungen -> Erweitert ->Verschlüsselung ->Zertifikate anzeigen -> Ihre Zertifikate -> Importieren…

Dort wählt man die michael.p12 Datei aus, gibt das Export-Passwort ein, drückt 2 Mal OK und wird beim erneuten Betreten der Seite gefragt ob man das Client-Zertifikat verwenden möchte:

In Chrome importiert man das Client-Zertifikat unter

Optionen -> Details -> HTTPS/SSL -> Zertifikate verwalten

In PHP können wir nun auf die Client-Zertifikats-Informationen zugreifen indem wir in das $_SERVER Array reinschauen, darin sind nun etliche Einträge enthalten die uns verraten wer sich gerade angemeldet hat:

array(78) {
  ["HTTPS"]=>
  string(2) "on"
  ["SSL_TLS_SNI"]=>
  string(17) "phpgangstatest.de"
  ["SSL_SERVER_S_DN_C"]=>
  string(2) "DE"
  ["SSL_SERVER_S_DN_ST"]=>
  string(3) "NRW"
  ["SSL_SERVER_S_DN_L"]=>
  string(5) "Oelde"
  ["SSL_SERVER_S_DN_O"]=>
  string(7) "private"
  ["SSL_SERVER_S_DN_CN"]=>
  string(14) "Michael Kliewe"
  ["SSL_SERVER_S_DN_Email"]=>
  string(18) "XXXX@phpgangsta.de"
  ["SSL_SERVER_I_DN_C"]=>
  string(2) "DE"
  ["SSL_SERVER_I_DN_ST"]=>
  string(3) "NRW"
  ["SSL_SERVER_I_DN_L"]=>
  string(5) "Oelde"
  ["SSL_SERVER_I_DN_O"]=>
  string(7) "private"
  ["SSL_SERVER_I_DN_CN"]=>
  string(14) "Michael Kliewe"
  ["SSL_SERVER_I_DN_Email"]=>
  string(18) "XXXX@phpgangsta.de"
  ["SSL_CLIENT_S_DN_C"]=>
  string(2) "DE"
  ["SSL_CLIENT_S_DN_ST"]=>
  string(3) "NRW"
  ["SSL_CLIENT_S_DN_O"]=>
  string(7) "private"
  ["SSL_CLIENT_S_DN_CN"]=>
  string(14) "Michael Kliewe"
  ["SSL_CLIENT_I_DN_C"]=>
  string(2) "DE"
  ["SSL_CLIENT_I_DN_ST"]=>
  string(3) "NRW"
  ["SSL_CLIENT_I_DN_L"]=>
  string(5) "Oelde"
  ["SSL_CLIENT_I_DN_O"]=>
  string(7) "private"
  ["SSL_CLIENT_I_DN_CN"]=>
  string(14) "Michael Kliewe"
  ["SSL_CLIENT_I_DN_Email"]=>
  string(18) "XXXX@phpgangsta.de"
  ["SSL_VERSION_INTERFACE"]=>
  string(14) "mod_ssl/2.2.20"
  ["SSL_VERSION_LIBRARY"]=>
  string(14) "OpenSSL/1.0.0e"
  ["SSL_PROTOCOL"]=>
  string(5) "TLSv1"
  ["SSL_SECURE_RENEG"]=>
  string(4) "true"
  ["SSL_COMPRESS_METHOD"]=>
  string(4) "NULL"
  ["SSL_CIPHER"]=>
  string(23) "DHE-RSA-CAMELLIA256-SHA"
  ["SSL_CIPHER_EXPORT"]=>
  string(5) "false"
  ["SSL_CIPHER_USEKEYSIZE"]=>
  string(3) "256"
  ["SSL_CIPHER_ALGKEYSIZE"]=>
  string(3) "256"
  ["SSL_CLIENT_VERIFY"]=>
  string(7) "SUCCESS"
  ["SSL_CLIENT_M_VERSION"]=>
  string(1) "3"
  ["SSL_CLIENT_M_SERIAL"]=>
  string(4) "1001"
  ["SSL_CLIENT_V_START"]=>
  string(24) "Feb  9 22:37:08 2012 GMT"
  ["SSL_CLIENT_V_END"]=>
  string(24) "Feb  8 22:37:08 2013 GMT"
  ["SSL_CLIENT_V_REMAIN"]=>
  string(3) "365"
  ["SSL_CLIENT_S_DN"]=>
  string(40) "/C=DE/ST=NRW/O=private/CN=Michael Kliewe"
  ["SSL_CLIENT_I_DN"]=>
  string(80) "/C=DE/ST=NRW/L=Oelde/O=private/CN=Michael Kliewe/emailAddress=XXXX@phpgangsta.de"
  ["SSL_CLIENT_A_KEY"]=>
  string(13) "rsaEncryption"
  ["SSL_CLIENT_A_SIG"]=>
  string(21) "sha1WithRSAEncryption"
  ["SSL_SERVER_M_VERSION"]=>
  string(1) "1"
  ["SSL_SERVER_M_SERIAL"]=>
  string(16) "A123F59C7E1E781B"
  ["SSL_SERVER_V_START"]=>
  string(24) "Feb  9 21:25:30 2012 GMT"
  ["SSL_SERVER_V_END"]=>
  string(24) "Feb  8 21:25:30 2013 GMT"
  ["SSL_SERVER_S_DN"]=>
  string(80) "/C=DE/ST=NRW/L=Oelde/O=private/CN=Michael Kliewe/emailAddress=XXXX@phpgangsta.de"
  ["SSL_SERVER_I_DN"]=>
  string(80) "/C=DE/ST=NRW/L=Oelde/O=private/CN=Michael Kliewe/emailAddress=XXXX@phpgangsta.de"
  ["SSL_SERVER_A_KEY"]=>
  string(13) "rsaEncryption"
  ["SSL_SERVER_A_SIG"]=>
  string(21) "sha1WithRSAEncryption"
  ["SSL_SESSION_ID"]=>
  string(64) "212FA56ECCB33E54CBC3F07DEEA0071696AC96DFFD02EF8BF6271E9545D8149C"
   ...
}

Wichtig dabei ist dass SSL_CLIENT_VERIFY auf SUCCESS steht, sollte der Wert “NONE” sein ist ein Benutzer ohne Zertifikat vorbeigekommen. Um das Zertifikat wiederzuerkennen eignen sich die SSL_CLIENT_M_SERIAL zusammen mit SSL_CLIENT_I_DN wohl am besten, diese sind einzigartig innerhalb der CA. Natürlich sollte man auch nicht vergessen zu prüfen ob das Zertifikat noch gültig ist. Dazu prüft man ob SSL_CLIENT_V_REMAIN > 0 ist, bzw. SSL_CLIENT_V_START und SSL_CLIENT_V_END .

Man sollte in der PHP-Applikation eine Datenbanktabelle haben in der die Zertifikatsinformationen zum entsprechenden Benutzer zugeordnet sind. Der Einfachheit halber kann man auch noch SSL_CLIENT_S_DN_CN und SSL_CLIENT_S_DN_Email speichern, dann ist es eventuell bequemer die Zertifikate zu administrieren.

Übrigens ist es auch möglich .htaccess mit den Client-Zertifikaten zu kombinieren.

Weitere Informationen zu diesem spannenden Thema:

http://cweiske.de/tagebuch/ssl-client-certificates.htm
http://www.noatun.net/docs/ssl_client.html#7
http://www.vanemery.com/Linux/Apache/apache-SSL.html

Nginx:
http://blog.nategood.com/client-side-certificate-authentication-in-ngi

Ähnliche Artikel:

  1. Client-IP Problem bei Reverse-Proxy-Betrieb
  2. PDT 2.1 unter Windows mit Subclipse SVN Client
Über den Autor

PHP Gangsta

Der zweitgrößte deutsche, eher praxisorientierte PHP-Blog von Michael Kliewe veröffentlicht seit Mitte 2009 Artikel für Fortgeschrittene.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen