X509 Clientzertifikate

reinhard@finalmedia.de Sun 04 Feb 2024 09:20:39 AM CET

Es Wurde bereits anderenorts viel darüber geschrieben, teilweise unvollständig, teilweise auf ältere openssl Versionen mit älterer Syntax. Hiermit reihe ich mich ein.

Hier nun ein komplettes Beispiel inkl. einer Apache Config.

Ich gehe davon aus, dass du auf deinem apache z.B. schon reguläres SSL am Start hast, z.B. mit Lets-Encrypt (acme), es also https://domainname.de gibt und du schon mal einen Apache Vhost konfiguriert hast.

Warum überhaupt?

Du möchtest nun zusätzlich zu den bestehenden Server-Zertifikaten für einzelne spezielle Unterordner deines Vhosts domainname.de auch Client-Zertifikate verwenden, die dann notwendig sind um auf diese Unterordner überhaupt zugreifen zu können.

Das bedeutet, dass später jeder Besucher ein eigenes zusätzliches X509 Zertifikat von dir erhalten wird, auf einem separaten Weg (anderer Kanal) und dies dann in seinen Browser importiert. Damit kann sich der Besucher gegenüber dem Server also ebenso ausweisen, und nicht nur der Server beim Besucher. Man kann das also zusätzliche Absicherung eines Logins verwenden - wenn man so will also ein zweiter Faktor, der im Hintergrund dann automatisch mitkommt.

Wenn dein Besucher dieses Zertifikat in seinen Browser importiert, entsperrt er es einmalig (symmetrisch verschlüsselte .p12 Datei) die du ihm zugeschickt hast und übernimmt diese in seinen Zertifikat-Storage im Browser. Das muss er manuell tun. Von da an ist er für dich eindeutig identifiziert, wenn er auf deinen Server zugreift. Serverseitig kannst du dann auch die im Zertifikat definierten Klarnamen, Serials etc. prüfen und weiter prozessieren, z.B. in eigenen Headern zu Proxy etc. und weitere Fallunterscheidungen treffen. Damit das geht hinterlegst du also auf dem Server nur einmalig den publickey deiner eigenen CA, gegen die der Server selbst prüfen kann. Das musst du dann auch nicht ständig erneuern, wie das bei acme der Fall ist.

Das Ganze soll ohne Terrorpanik-Meldung geschehen, obwohl du self-signed Certs und CA verwendest. Und genau das funktioniert auch wunderbar, wenn du als Serverzertifikate die acme certs verwendest, aber als clientzertifikate und cacert deine selbstsignierten X509 Zertifikate.

Schritt 1 - Die eigene CA

Damit das geht, musst du (also derjeninge, der künftig diese Clientzertifikate an dein Nutzer ausgeben wird) zunächst ein selfsigned CA Cert erstellen. Damit bist du die Certificate Authority für deine künftigen Clientkeys. Wir schützen deinen Privatekey jetzt nicht großartig. Sorge selbst dafür. Ergo: Die ca.key niemals rausgeben! Nur die ca.pem darf nach außen. Erstelle daher das hier nicht auf deinem Webserver, sondern nur auf deinem privaten ggf. gesonders geschützten Rechner. Der Besitzer der ca.key kann jederzeit neue clientkeys erzeugen. Am Server muss dann auch nichts mehr geändert werden. So.. nun das hier bitte auf deinem geschützten Rechner

	mkdir meine_eigene_ca
	cd meine_eigene_ca
        openssl req -new -newkey rsa:4096 -nodes -out ca.csr -keyout ca.key -sha256
        openssl x509 -trustout -signkey ca.key -days 3650 -req -in ca.csr -out ca.pem -sha256

Das ist damit für 10 Jahre gültig. Du kannst das aber selbst anpassen.

Schritt 2 - Die Client Certs

Wir erstellen hier eines für client01. Für weitere Clients ersetzt du das also einfach an allen Stelle 01 zu 02 und so weiter. Auch das geschieht auf deinem eigenen geschützten Rechner, nicht auf dem Webserver.

	name="client01"
	serial="$(date +%Y%m%d%H%M%S)"
        openssl genrsa -out ${name}.key 4096
        openssl req -new -key ${name}.key -out $name.csr -sha256
        openssl x509 -req -days 365 -in ${name}.csr -CA ca.pem -CAkey ca.key -set_serial ${serial} -out ${name}.cer -sha256
        openssl pkcs12 -export -inkey ${name}.key -name "user" -in ${name}.cer -certfile ca.pem -out ${name}.p12

Hinweis: Im letzten Schritt setzt du ein Passwort, um die .p12 zu schützen. Der Nutzer muss das beim Import später eingeben. Du setzt es also für den Nutzer und es dient als zusätzlicher Schutz, falls dem Nutzer die .p12 Datei abhanden kommt.

Schritt 3 - ca.pem zu deinem Webserver übertragen

Auf dem Server muss nun die ca.pem liegen. Denn der Server muss ja gegen diesen vertrauten publickey prüfen, wenn deine clients anfragen. Apache sieht dabei den Hash des Zertifikats mit dem der Client anfragt, bzw. gegen welches public cert er das anwenden und vergleichen muss um ein "ok" oder ein "du kommst hier nicht rein" zu liefern.

Wir erstellen hier auf dem Server daher ein neues Verzeichnis und darunter gleich ein unterverzeichnis für eine Domain

	mkdir /etc/clientcert_ca_storage
	mkdir /etc/clientcert_ca_storage/domainname.de
	chmod 750 /etc/clientcert_ca_storage
	chmod 750 /etc/clientcert_ca_storage/domainname.de
	chown -R root:www-data  /etc/clientcert_ca_storage/domainname.de

Das Verzeichnis muss von apache lesbar sein, da er ja zur Laufzeit dort dann hashes gegen die ca prüft. Nur relevant, wenn du über .htacess Files arbeitest. Machst du das im Vhost, wird das eh nur beim Start des Apache und dortigen und Parsen der V-Host Config eingelesen. Was auch performanter und sicherer ist.

Deine ca.pam musst du nun dort einmalig reinlegen.

	scp ca.pem user@deinserver:/etc/clientcert_ca_storage/domainname.de/ca.pem

Und dann musst du die Hashlinks erstellen. Das geht mit dem Befehl c_rehash, der über das openssl Paket auf dein System kam:

	 c_rehash /etc/clientcert_ca_storage/domainname.de/

Auch das müssen wir in den 10 Jahren dann nur einmal tun. Auch dann nicht, wenn wir später neue clientcerts machen. Die sind ja dann alle dieser CA untergeordnet.

Schritt 4 - Konfiguration deines Apachen


<VirtualHost *:443>

	Servername domainname.de
	Serveralias www.domainname.de
	Serveradmin webmaster@domainname.de

	SSLProxyEngine On                                                                                                                                                                           
        SSLEngine on                                                                                                                                                                                

        SSLProtocol -All +TLSv1.2 +TLSv1.3
	SSLStrictSNIVHostCheck off
	SSLHonorCipherOrder on
	SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+AESGCM EECDH EDH+AESGCM EDH+aRSA HIGH !MEDIUM !LOW !aNULL !eNULL !LOW !RC4 !MD5 !EXP !PSK !SRP>
	SSLCompression Off

	# Die nachfolgenden Definition wirst du schon kennen. Da musst du auch nix ändern. 
	# Dein Lets Encrypt bleibt unangetastet...

	SSLCertificateFile /pfad/zu/acme/domainname.de/domainname.de.cer
	SSLCertificateKeyFile	/pfad/zu/acme/domainname.de/domainname.de.key
	SSLCertificateChainFile	/pfad/zu/acme/domainname.de/fullchain.cer
	SSLCACertificateFile /pfad/zu/acme/domainname.de/ca.cer


	# SOOO! Ab hier ist neu!
	# Client Auth SelfSigned CA Certs liegen hier und wurden mit c_rehash /etc/clientcert_ca_storage/domainname.de/  gehashed!
	# Hier wird also ein Verzeichnis(!) angegegeben und Dateien die dort abgelegt wurden müssen mit obigem c_rehash Befehl erst automatisch
	# gesymlinkt werden, weil Apache unter dem Hash als Filename nachsieht

        SSLCACertificatePath /etc/clientcert_ca_storage/domainname.de/


	# Jetzt definieren wir, für welches Verzeichnis wir die Einschränkung erzwingen

	<Location "/protectedarea/">

                ErrorDocument 403 'Login nur mit speziellem X509 Clientzertifikat gestattet.'

		SSLVerifyClient require

                SSLVerifyDepth 1
                SSLOptions +StdEnvVars +ExportCertData
                SSLUserName SSL_CLIENT_S_DN_CN

                # fuer proxy ggf. durchreichen. Ergänze selbst ggf. andere Infos
                RequestHeader set X-SSL-CLIENT-S-DN-O "%{SSL_CLIENT_S_DN_O}s"
                RequestHeader set X-SSL-CLIENT-S-DN-CN "%{SSL_CLIENT_S_DN_CN}s"

                # dem client im header auch seine eigenen infos zurueckgeben. Machen wir nur zu debug zwecken
		# dazu verwenden wir eigene X Header Namen

                Header always set X-CURRENT-TIMESTAMP "%{TIME_YEAR}s%{TIME_MON}s%{TIME_DAY}s%{TIME_HOUR}s%{TIME_MIN}s%{TIME_SEC}s"
                Header always set X-YOUR-O "%{SSL_CLIENT_S_DN_O}s"
                Header always set X-YOUR-CN "%{SSL_CLIENT_S_DN_CN}s"
                Header always set X-CERT-START "%{SSL_CLIENT_V_START}s"
                Header always set X-CERT-END "%{SSL_CLIENT_V_END}s"
                Header always set X-CERT-REMAIN "%{SSL_CLIENT_V_REMAIN}s"
                Header always set X-CERT-END "%{SSL_CLIENT_V_END}s"
                Header always set X-CERT-SERIAL "%{SSL_CLIENT_M_SERIAL}s"
                Header always set X-CERT-VERSION "%{SSL_CLIENT_M_VERSION}s"

                Options -Indexes
                AllowOverride None
                Require ssl-verify-client
                Require valid-user


		# Natürlich kannst du dann hier noch weiter einschränken mit weiteren Require 
		# Weiteren Auth gegen Userdatenbanken und erlaubten Methoden. Beispielweise
                AllowMethods GET HEAD
		
		# Den Username haben wir ja im clientcert hinterlegt, können den hier auch auslesen
		# wie oben mit SSLUserName und der passenden Referenz auf den Wert im Zertifikat zu sehen
		# Daraufhin kann man hier dann weiteren Auth durchführen, z.B. gegen LDAP, lokale Dateien
		# o.ä. wiederum diesen Usernamen prüfen. Da wir die clientcerts ja selbst erstellt und signiert
		# haben, gewährleisten wir auch deren Integrität. Der dort hinterlegte username ist also von
		# uns vorgegeben und der Besitzer des clientzertifikats kann das nicht ändern. 
		# Da wir auch die Gültigkeitsdauer des Zertifikats auslesen können (siehe header oben)
		# können wir den Nutzer auch rechtzeitig vorher über einen drohenden Ablauf informieren.
		# Diese Variable ist ja auswertbar und kann dann im Header auch mitgegeben werden.

	</Location>

</VirtualHost>

Teste nun deine Apache Config und aktiviere die Config in deinem Server

	apache2ctl -t
	service apache2 reload

Schritt 5 - Verbindung Testen

Auf der clientseite kannst du mit curl testen. Curl kann sowohl die Variante mit .cer und .key getrennt, als auch mit einer .p12 datei, bei der du dann das symmetrische Zusatzkennwort mitübergibst:

	curl -vvv --tlsv1.3 --cert client01.cer --key client01.key https://domainname.de/protectedarea/
	curl -vvv --tlsv1.3 --cert-type P12 --cert client01.p12:hast_du_selbst_gesetzt https://domainname.de/protectedarea/

Wir haben auf -vvv übergeben, um mehr Debug Informationen zu sehen, z.B. auch die Response Header die uns unser Webserver zurückliefert. Daran sehen wir auch, welches Clientzertifikat verwendet wird

Schritt 6 - Firefox

Firefox kann in älteren Versionen keinen post_handshake bei client-zertifikaten, wenn das moderne TLS 1.3 im Einsatz ist, wie in unserem Beispiel.

Dann muss man am Firefox noch was anpassen, also about:config aufrufen, und dort

security.tls.enable_post_handshake_auth
auf den Wert
true
stellen.

Warum ist das so? So viel ich weiß post_handshake_auth mit http/1.1 funktioniert, aber bei http/2 mit multiplexing Probleme macht und daher der entsprechende Mechanismus erst noch in der Entwurfsphase ist.

Aber warum ist da so in Firefox und warum geht das nicht anders? Siehe https://bugzilla.mozilla.org/show_bug.cgi?id=1511989 und https://bugzilla.mozilla.org/show_bug.cgi?id=1637754

Zudem sollte du

security.default_personal_cert
auf den Wert "Ask Every Time" stellen.

Reicht aber nicht... Firefox und Chrome Bug bei TLS v1.3. Siehe hier