gRPC Tutorial Teil 4: HTTP/2 über HTTPS
gRPC benutzt HTTP/2, welches 2015 veröffentlicht wurde und binäre Kommunikation unterstützt. Moderne Browser unterstützen HTTP/2 ausschliesslich über eine gesicherte Verbindung. Ein gRPC-Client kann dagegen problemlos über eine ungesicherte Verbindung mit einem gRPC-Server kommunizieren. Für Autorisierungstechniken wie z.B. JWT Bearer Tokens (siehe Blog über JWT Client Credentials) muss die Verbindung zwingend gesichert sein. In diesem Blog wird demonstriert, wie man den Kestrel-Server für eine gesicherter Verbindung konfiguriert. Obwohl dieser Blog als Grundlage für einen späteren gRPC-Blog dient, funktioniert das Absichern genau gleich wie für eine REST-Schnittstelle oder eine normale Internetseite.
Basisdefinitionen
HTTP (Hypertext Transfer Protocol) ist ein Protokoll, das von Clients (z. B. Webbrowsern) verwendet wird, um Ressourcen von Servern (z. B. Webservern) anzufordern.
- HTTP/1.1 – ist seit 1999 das Äquivalent für HTTP und ist ein text basiertes Protokoll.
- HTTP/2.0 – wurde am 15. Mai 2015 veröffentlicht und unterstützt binäre Kommunikation, parallele Anfragen über einen TCP-Kanal und Header Kompression.
HTTPS ist eine Methode zum Verschlüsseln von HTTP. Grundsätzlich werden HTTP-Nachrichten in einem verschlüsselten Format unter Verwendung von SSL / TLS verpackt.
- SSL (Secure Sockets Layer) wurde von Netscape entwickelt und kam 1995 als Version 2.0 auf den Markt. Die POODLE Attacke wurde SSL 2014 zum Verhängnis.
- TLS (Transport Layer Security) wurde 1999 als Upgrade für SSL 3.0 definiert und ist seit 2006 auf dem Markt.
Eine gesicherte Verbindung läuft heutzutage über TLS. Zur Unterstützung älterer Browser bieten die meisten Server auch SSL an. Leider wird oft immer noch SSL gesagt, wenn eigentlich TLS gemeint ist (z.B. badssl.com).
Verschlüsseln und signieren
Um den Datenverkehr zu verschlüsseln, braucht es einen Schlüssel, welcher den Klartext in einen Geheimtext (Chiffrat) umwandeln kann und wieder zurück. Es gibt zwei grundlegende Verfahren sowie eine Kombination von beiden.
Bei der symmetrischen Verschlüsselung wird derselbe Schlüssel verwendet, um Botschaften zu verschlüsseln und auch wieder zu entschlüsseln. Der Schlüssel muss also sowohl dem Sender als auch dem Empfänger bekannt sein.
Bei der asymmetrischen Verschlüsselung, oder Public-Key-Verschlüsselungsverfahren, gibt es einen öffentlichen und einen privaten Schlüssel.
Daten, die mit dem öffentlichen Schlüssel verschlüsselt wurden, können nur mit dem dazugehörigen privaten Schlüssel entschlüsselt werden.
Quelle: https://de.wikipedia.org/wiki/asymmetrisches_kryptosystem
Das gleiche Schlüsselpaar wird zum Signieren verwendet. Zum Erstellen einer Signatur wird ein Hashwert aus der zu verschickenden Nachricht gebildet und mit dem privaten Schlüssel signiert. Nachricht und Signatur werden dann zum Empfänger geschickt. Zum Verifizieren der Signatur wird die empfangene Signatur des Hashwertes mit dem öffentlichen Schlüssel geprüft. Ist die Verifizierung erfolgreich, kann davon ausgegangen werden, dass die Nachricht vom Besitzer des privaten Schlüssels stammt und dass die Nachricht bei der Übertragung nicht (zufällig oder absichtlich) manipuliert wurde.
Ein Nachteil der asymmetrischen Verschlüsselung ist der hohe Aufwand für die Ver- und Entschlüsselung, was sich deutlich auf die Geschwindigkeit auswirkt.
Bei der hybriden Verschlüsselung wird mit der asymmetrischen Verschlüsselung die Authentizität des Servers (und optional Clients) überprüft und ein symmetrischer Schlüssel zur Verschlüsselung der nachfolgenden Daten übertragen. Dieser Ablauf findet beim Aufsetzen einer HTTPS-Verbindung statt, auch TLS Handshake genannt.
Das Aufsetzen einer HTTPS-Verbindung
Bevor eine gesicherte Verbindung zustande kommt, wird zwischen Client und Server verhandelt.
Quelle: https://www.ibm.com/support
- Der Client sendet eine «client hello»-Nachricht, in der u.a. die TLS-Version und die vom Client unterstützten Verschlüsselungsalgorithmen (CipherSuites) aufgelistet sind.
- Der Server antwortet mit einer «server hello»-Nachricht mit der gewählten CipherSuite, der Sitzungs-ID und dem Server-Zertifikat.
Optional: Wenn für den Server ein digitales Zertifikat für die Clientauthentifizierung erforderlich ist, sendet der Server eine Clientzertifikatsanforderung (siehe nächsten Blog über Client-Zertifikat Authentifizierung). - Der Client überprüft das digitale Zertifikat des Servers. Das Hauptziel ist es, sicherzustellen, dass der Server tatsächlich der Server ist, für den ihn der Client hält. Dieser Vorgang ist kompliziert und sprengt den Rahmen dieses Blogs.
- Der Client sendet die zufällige Bytefolge, die es sowohl dem Client als auch dem Server ermöglicht, den geheimen Schlüssel zu berechnen, der für die Verschlüsselung der nachfolgenden Nachrichtendaten verwendet werden soll. Die zufällige Bytefolge selbst wird mit dem öffentlichen Schlüssel vom Server-Zertifikat verschlüsselt.
- Optional: Wenn der Server eine Clientzertifikatsanforderung gesendet hat, sendet der Client eine zufällige Bytefolge, die mit dem privaten Schlüssel des Clients verschlüsselt wird, zusammen mit dem Client-Zertifikat des Clients.
- Optional: Der Server überprüft das Client-Zertifikat.
- Der Client sendet dem Server eine «Finished»-Nachricht.
- Der Server sendet dem Client eine «Finished»-Nachricht.
- Für die Dauer der TLS-Sitzung können jetzt Server und Client Nachrichten austauschen, die symmetrisch mit dem geheimen Schlüssel für gemeinsame Nutzung verschlüsselt sind.
Root- und Server-Zertifikat
Um die HTTPS-Verbindung aufsetzen zu können, braucht es ein Server-Zertifikat. Das Server-Zertifikat befindet sich am Ende der Zertifizierungskette. Am Anfang der Kette befindet sich das Root-Zertifikat, welches verwendet wird, um die Echtheit des Zertifikates und damit auch die Authentizität des Servers zu beweisen.
Ein digitales Client-Zertifikat ist im Grunde eine Datei, die mit einem Passwort geschützt und in eine Client-Anwendung geladen wird (normalerweise als PKCS12-Datei mit der Erweiterung ‘.p12’ oder ‘.pfx’, die Datei kann mit z.B. OpenSSL konvertiert werden in einer .crt/.cer und .key Datei. Letztere Datei enthält den private Key).
Ein digitales Zertifikat enthält relevante Informationen wie eine digitale Signatur, Ablaufdatum, Name des Issuers, Name der CA (Certificate Authority), Sperrstatus (revocation), SSL/TLS-Versionsnummer, Seriennummer und möglicherweise weitere Informationen, die alle nach dem X.509-Standard strukturiert sind.
Selbstsignierte Zertifikate erstellen
Normalerweise werden Zertifikaten von Zertifizierungsstellen (‘certificate authority’ oder ‘certification authority’, kurz CA) ausgestellt (z.B. Verisign, Thawte, etc.). Für unser Beispiel werden wir selber die Zertifikate generieren. Es gibt verschiedene Wege selber Zertifikate zu generieren: Powershell, Online Tools, OpenSSL etc. In diesem Beitrag wird OpenSSL verwendet.
OpenSSL installieren
Die binären Dateien des OpenSSL Tools stehen als ZIP-Datei zur Verfügung auf ‘https://sourceforge.net/projects/openssl‘ oder als Installer auf ‘https://slproweb.com/products/Win32OpenSSL.html‘. Dazu die letzte Version des Installers ‘Win64OpenSSL_Light-1_1_0L.exe’ (oder höher) herunterladen und den Installer ausführen. Wichtig ist, dass ‘openssl.exe’ und ‘openssl.cnf’ sich nach der Installation im Verzeichnis ‘C:\OpenSSL-Win64\bin’ befinden.
Zertifikate generieren
Im Beispiel gibt es einen Unterordner ‘Certs’ in dem alle OpenSSL Tool Input- und Output-Dateien gespeichert werden.
Die Inhalte des Zertifikatfeldes ‘Enhanced Key Usage’ müssen für das OpenSSL Tool in einer externen Datei definiert werden. In diesem Verzeichnis eine Textdatei ‘openssl-ext.cnf’ mit folgendem Inhalt erstellen:
[server_ssl] extendedKeyUsage = serverAuth
Eine zweite Datei namens ‘create_certs.cmd’ mit folgendem Inhalt erstellen:
set OPENSSL_CONF=c:\OpenSSL-Win64\bin\openssl.cfg echo Generate CA key: c:\OpenSSL-Win64\bin\openssl genrsa -passout pass:P@ssw0rd -des3 -out root.key 4096 echo Generate CA certificate: c:\OpenSSL-Win64\bin\openssl req -passin pass:P@ssw0rd -new -x509 -days 3650 -key root.key -out root.crt -subj "/C=CH/ST=LU/L=Root D4/O=Noser Engineering AG/OU=www.noser.com/CN=Noser RpcWithCertificates Root" echo Generate server key: c:\OpenSSL-Win64\bin\openssl genrsa -passout pass:P@ssw0rd -des3 -out server.key 4096 echo Generate server signing request: c:\OpenSSL-Win64\bin\openssl req -passin pass:P@ssw0rd -new -key server.key -out server.csr -subj "/C=CH/ST=LU/L=Root D4/O=Noser Engineering AG/OU=www.noser.com/CN=localhost" echo Self-sign server certificate: c:\OpenSSL-Win64\bin\openssl x509 -req -passin pass:P@ssw0rd -days 3650 -extensions server_ssl -extfile openssl-ext.cnf -in server.csr -CA root.crt -CAkey root.key -set_serial 01 -out server.crt echo Remove passphrase from server key: c:\OpenSSL-Win64\bin\openssl rsa -passin pass:P@ssw0rd -in server.key -out server.key c:\OpenSSL-Win64\bin\openssl pkcs12 -export -in root.crt -inkey root.key -out root.pfx -passout pass:P@ssw0rd -passin pass:P@ssw0rd -name "Noser Engineering self-signed root cert" c:\OpenSSL-Win64\bin\openssl pkcs12 -export -in server.crt -inkey server.key -out server.pfx -passout pass:P@ssw0rd -name "Noser Engineering self-signed server cert" pause
Nach dem Ausführen des oberen Skripts hat das Verzeichnis folgenden Inhalt:
Das Skript generiert verschiedene (Zwischen-)Dateien, wovon für uns nur 2 wichtig sind:
- root.pfx (friendly name ‘Noser Engineering self-signed root cert’)
- server.pfx (friendly name ‘Noser Engineering self-signed server cert’)
Die Datei ‘root.pfx’ enthält das Zertifikat mit ‘Intended Purposes All’ und wird zum Signieren des Server- und Client-Zertifikates verwendet. Das Server-Zertifikat hat ‘Intended Purposes Server Authentication’ und wird später im gRPC Server geladen. Die Zertifikate müssen aber zuerst auf dem Rechner installiert werden.
Installieren der Zertifikate
Da es nicht möglich ist, mit OpenSSL den ‘friendly name’ in einer ‘crt’-Datei zu generieren, verwenden wir die ‘pfx’-Datei zum Installieren der Zertifikate.
Doppelklick auf root.pfx und ‘Local Machine’ wählen.
Das Passwort einfügen (P@ssw0rd).
Für das Root-Zertifikate den ‘Trusted Root Certification Authorities’ Store auswählen.
Alle Schritte für ‘server.pfx’ wiederholen, mit dem Unterschied, dass ‘Intermediate Certification Authorities’ als Store gewählt werden soll.
Danach die Microsoft Management Konsole öffnen durch Eingabe von ‘cert’ im Windows Startmenü. Es erscheint ‘Manage computer certificates’ in den Ergebnissen, welches die Konsole öffnet.
Das Root-Zertifikat soll sich jetzt in ‘Trusted Root Certification Authorities’ befinden.
Und das Server-Zertifikat in ‘Intermediate Certification Authorities’.
gRPC Server Einrichten
In diesem Abschnitt wird der Kestrel-Server für eine HTTPS-Verbindung konfiguriert. Dazu wird das Server-Zertifikat gebraucht, welches gerade generiert und installiert wurde. Das Beispielprojekt ‘RpcOverHTTPSServer’ dient als Grundlage für die nächsten Blogs und kann hier als ZIP-Datei heruntergeladen werden.
Die Datei ‘server.pfx’ muss ins Serverprojekt kopiert und beim Builden ins Ausgabeverzeichnis kopiert werden.
Passwort und Dateiname werden in appsettings.json vom Serverprojekt in einer neuen Sektion ‘Certificate’ definiert:
{ "Logging": { … }, "Certificate": { "File": "server.pfx", "Password": "P@ssw0rd" } }
In der program.cs Datei kann jetzt der Kestrel-Server für eine HTTPS-Verbindung konfiguriert werden:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.ConfigureKestrel( opt => { var config = (IConfiguration)opt.ApplicationServices.GetService(typeof(IConfiguration)); var cert = new X509Certificate2( config["Certificate:File"], config["Certificate:Password"]); opt.ConfigureHttpsDefaults( h => { h.ClientCertificateMode = ClientCertificateMode.NoCertificate; h.CheckCertificateRevocation = false; h.ServerCertificate = cert; }); }); });
Der KestrelServerOptions-Parameter von ConfigureKestrel() enthält eine Instanz des DI-Containers, welche gebraucht wird, um die Implementation von IConfiguration zu holen. Mit IConfiguration kann der Dateiname und das Passwort des Zertifikates aus appsettings.json gelesen werden und eine X509-Zertifikat Instanz erstellt werden.
Die HTTPS-Verbindung wird ohne Client-Zertifikat Authentifizierung erstellt (ClientCertificateMode.NoCertificate). Client-Zertifikat Authentifizierung wird im nächsten Blog erklärt und sorgt dafür, dass der Server den Client authentifiziert, bevor der Client auf die Schnittstellen zugreifen darf. Damit wird verhindert, dass unbekannte Clients auf den Server zugreifen.
Weil das Zertifikat selbstsigniert ist, kann es nicht durch eine Zertifizierungsinstanz widerrufen werden. Ausserdem fehlt beim Zertifikat die URL, um kontrollieren zu können (OCSP – Online Certificate Status Protocol), ob das Zertifikat widerrufen wurde. Deshalb wird CheckCertificateRevocation auf false gesetzt.
Für lokale Tests gibt es die Datei ‘Properties\launchSettings.json’ welche beschreibt wie der Server gestartet werden soll. Wenn der Server mit ‘dotnet run’ gestartet wird, wird die Konfiguration mit ‘commandName: Project’ genommen. Wichtig ist, dass die applicationUrl auf https://localhost:5001 gesetzt wird und der Standard http://localhost:5000 gelöscht wird.
{ … "RpcOverHTTPS": { "commandName": "Project", "launchBrowser": false, "launchUrl": "", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
Jetzt den Server via Visual Studio 2019 starten oder im Command-Prompt zum Root des Projektes navigieren und dort tippen ‘dotnet run’.
Im Ausgabefenster steht jetzt ‘Now listening on: https://localhost:5001’.
gRPC Client
Der Client ist eine einfache Sache: Man muss nur den Kanal für ‘https://localhost:5001’ erstellen. Im Beispiel ist diese URL ausgelagert in appsettings.json (‘Service:ServiceUrl’).
protected DemoService.DemoServiceClient Client { get { if (_client == null) { var handler = new HttpClientHandler(); var client = new HttpClient(handler); var opt = new GrpcChannelOptions { HttpClient = client, LoggerFactory = _loggerFactory }; ChannelBase channel = GrpcChannel.ForAddress(_config["Service:ServiceUrl"], opt); _client = new DemoService.DemoServiceClient(channel); } return _client; } }
Der Client verbindet sich wie vorher mit dem Server, nur jetzt über eine gesicherte Verbindung.
Fazit
Standardmässig benutzt gRPC das HTTP/2 Protokoll über eine ungesicherte Verbindung. Dieses Beispiel zeigt, wie der Kestrel-Server für eine sichere Verbindung eingerichtet werden kann. Das Absichern selber funktioniert gleich wie für eine REST-Schnittstelle oder eine normale Internetseite. In einem der nächsten Blogs wird Authentifizierung und Autorisierung mit JWT Tokens erklärt. Wer das Token besitzt kann mit dem Token auf den Server zugreifen. Deshalb ist für JWT Token Authentifizierung eine gesicherte Verbindung eine absolute Voraussetzung.