PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Client-Zertifikate als sicherer Login-Ersatz?

with 19 comments

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

Written by Michael Kliewe

Februar 11th, 2012 at 11:13 am

19 Responses to 'Client-Zertifikate als sicherer Login-Ersatz?'

Subscribe to comments with RSS or TrackBack to 'Client-Zertifikate als sicherer Login-Ersatz?'.

  1. Schade ich hätte mir eine Serverkonfiguration mittels mod_gnutls gewünscht, da gerade dieses Probleme bei Client-Zertifikaten bereitet und von den meisten Linux Distributionen genommen wird, da es unproblematischer von der Lizenz ist.
    Denn dies scheint niemand beantworten zu können, die alten mod_ssl Konfigurationen sind relativ einfach um zu setzen, aber viele Sachen sind einfach veraltet, des3 ist nicht so wirklich die favorisierte Verschlüsselung. 😉

    Sascha Ahlers

    11 Feb 12 at 14:11

  2. Ich benutze das übrigens gerne. Cherokee hat dafür auch ein hübsches Interface. Ich persönlich würde den RSA Key noch auf 4048 setzen, muss man aber nicht. Warum bist Du denn auf die Camellia gegangen?

    Leider gibt es derzeit kaum eine Möglichkeit das soweit zu automatisieren und es beim Login zu benutzen. 🙁

    @Sascha: Was ist denn an Triple DES falsch?

    Oliver

    11 Feb 12 at 22:52

  3. Die Schlüsselstärke ist für aktuelle Anwendungsfälle mittlerweile zu gering:
    http://de.wikipedia.org/wiki/Triple_DES#Triple-DES

    3DES = 168 Bit (effektiv 112 Bit)

    Sichere Verbindungen sollten auf 256 Bit aufbauen, alles darunter sollte als bedenklich angesehen werden.

    Sascha Ahlers

    12 Feb 12 at 20:16

  4. Auch wenn da ein oder andere hier was zu bemängeln hat, finde ich diesen Artikel lobenswert. Dieser schafft den richtigen Einstieg in die Materie. Natürlich ist der Artikel noch ein wenig ausbaufähig, aber in diesem Bereich gibt es relativ wenig welche auch noch zusätzlich in Deutsch verfasst sind. Daher von mir aus Daumen hoch und macht weiter so.

    Nico Schubert

    13 Feb 12 at 08:56

  5. @Oliver: Ich habe nichts spezielles ausgewählt wie man oben sieht, es scheint also der Standard-Cipher zu sein den Ubuntu da nimmt.

    @Nico: Ich finde es gut dass Verbesserungsvorschläge kommen, nichts was ich hier schreibe ist perfekt und sollte 1:1 übernommen werden ohne nachzudenken. Gerade dieses Thema habe ich auch selbst (noch) nicht im Einsatz, mich hat es nur interessiert es mal auszuprobieren und das System ans Laufen zu bekommen. Sicher kann man das Vorgehen noch verbessern, wie beispielsweise ein eigenes CA-Zertifikat nur für die Client-Zertifikate, oder längere Schlüssel, es geht immer besser, und ihr sollt ja auch euren Beitrag dazu leisten 😉

    Michael Kliewe

    13 Feb 12 at 11:41

  6. Wiedermal ein toller Artikel. Wir nutzen die Client-Authorisierung per Zertifikat auch. Auf die von dir aufgezählten Zertifikatsdaten hat man aber meines Wissens nachnur Zugriff wenn PHP als Apache-Modul und nicht als CGI läuft. Ich lasse mich da aber auch sehr gerne eines besseren belehren 🙂

    Marcel Besancon

    14 Feb 12 at 12:42

  7. Danke für den tollen Artikel! 🙂

    Kannst du auch etwas dazu sagen/schreiben, wie man das in Verbindung mit PIN Eingabe durch ein Lesegerät realisieren kann? z.b. für Anmeldung an einem Web Portal? Oder kannst du mir zu dem Thema Infos geben?

    Besten Dank!

    Matthias

    17 Okt 12 at 19:10

  8. bzw. wird die PIN der Chipkarte von der Middleware im Browser abgefragt oder programmiert man diese Funktionalität in z.B. PHP?

    Matthias

    20 Okt 12 at 19:07

  9. Wie kann man sich überhaupt eigene Zertifikate machen?
    Bin dort drauf gegangen in Chrome und weiter weiß ich nicht.

    Alexandra

    26 Nov 12 at 23:04

  10. […] PHP-Gangsta hat mir bei der Client-Zertifikat-Authentifizierung mit seinem Blog sehr geholfen: PHP Gangsta – Client-Zertifikate als Login-Ersatz Allerdings hab ich meine CA etwas anders aufgesetzt und auch aes256 anstatt des3 verwendet. Da die […]

  11. Schöne Praxisanleitung, genau so wünscht man sich das. Googlen, nachmachen, läuft. Ohne 1000 Querverweise. Danke dafür, hat auf Anhieb geklappt!

    David

    4 Aug 15 at 04:19

  12. >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 …

    Dieser Satz suggeriert, dass die URL bei HTTPS-Verbindungen verschlüsselt übertragen würde – was aber nicht der Fall ist.

    Rainer

    19 Aug 15 at 23:09

  13. @Rainer: Bei HTTPS-Verbindungen wird die volle URL erst übertragen nachdem die Verbindung verschlüsselt aufgebaut wurde. Lediglich die Domain wird (bedingt durch SNI) im Klartext übertragen. Man sieht also dass ich auf spiegel.de bin, aber man sieht nicht welchen Artikel ich lese, in welchem Forum ich lese oder wo ich einen Kommentar poste…

    SNI: https://de.wikipedia.org/wiki/Server_Name_Indication
    Weitere Infos und Antworten zum Thema:
    https://www.google.de/search?q=https+URL+verschlüsselt

    Michael Kliewe

    19 Aug 15 at 23:22

  14. Ein fantastischer Artikel – es hat alles auf Anhieb funktioniert. Einfach großartig!

    Me

    3 Okt 15 at 08:43

  15. Nette Anleitung. Da fragt man sich nur unweigerlich, warum der Webserver, der phpgangsta.de hostet, keine SSL Verschlüssleung anbietet, sondern nur http. 😀

    YamYamL

    27 Apr 17 at 16:44

  16. @YamYamL Danke für den letzten Anstoss, der fehlte mir noch, mich endlich um „den Rest“ zu kümmern 🙂

    Siehe https://www.phpgangsta.de/blog-mittels-lets-encrypt-dauerhaft-via-ssl-erreichbar

    Michael Kliewe

    4 Mai 17 at 10:05

  17. […] Kommentar von YamYamL hat mich daran erinnert, die HTTPS-Umstellung meiner Domain endlich abzuschliessen, und noch einen […]

  18. Mir ist beim testen aufgefallen das die Zeile

    genrsa -des3 -out michael.key

    so heissen muss .

    openssl genrsa -des3 -out michael.key

    Rakon Dark

    18 Aug 17 at 14:25

  19. Great article!

    But I got stuck… neither my Chrome nor Firefox accept CA / Certificate at all after importing the CA & p12 Cert on my Ubuntu machine and in each browser.

    I stuck getting this error:
    SSL_ERROR_UNKNOWN_CA_ALERT

    Any idea? Already tried a lot of „tricks“ that should fix it, but none worked.

    gkn

    19 Mrz 20 at 16:03

Leave a Reply

You can add images to your comment by clicking here.