Full Disclosure: Sicherung interner Kommunikation

Aktuelle E-Commerce-Systeme bestehen häufig aus mehreren Servern, die mit dem Browser der Benutzer über SSL-gesicherte Leitungen kommunizieren. Allerdings sind die Rechner der Kunden nicht die einzigen, mit denen ein Shop Daten austauschen muss: fast immer ist es notwendig, dass die einzelnen Server des Systems auch untereinander kommunizieren müssen. Dabei ist es unerheblich, ob es sich um eine Microservice– oder eine Vertikalen-Architektur handelt. Sogar die in die Tage gekommenen Monolithen nutzen bei einem Multi-Tier-Ansatz gerne mehrere Server.

Gemein ist diesen Backend-Requests (im Gegensatz zu dem Netzwerkverkehr zwischen Kunden-Browser und otto.de), dass hier im Allgemeinen keine vertraulichen Daten über die Leitung gehen, sondern beispielsweise die Aktualisierung einer Produktverfügbarkeit oder Konfigurations-Requests. Diese Inhalte müssen also nicht geheim gehalten werden, was uns den Aufwand und die Komplexität erspart, sie zu verschlüsseln.

Trotzdem darf natürlich nicht jeder die Texte im Shop ändern oder Produktinformationen anpassen, daher benötigen wir eine Methode zur Authentifizierung und Autorisierung der Requests.

Authentifizierung & Autorisierung

Diese Begriffe werden umgangssprachlich häufig synonym benutzt, aber für Techniker bezeichnen sie konzeptionell verschiedene Dinge:

Authentifizierung meint einen Vorgang, bei dem die Echtheit oder Identität von etwas festgestellt wird. In diesem Zusammenhang soll bei einem Request geprüft werden können, ob er von einem bekannten Benutzer bzw. Rechner initiiert wurde.

Bei der Autorisierung, die im Allgemeinen als darauffolgender Schritt vorgesehen ist, wird geprüft, ob der Benutzer bzw. Rechner die notwendigen Berechtigungen hat, die gewünschte Aktion auszuführen.

HMAC

Die HMAC-Methode wird in verschiedenen RFCs standardisiert. Wie alle MACs basiert sie darauf, dass eine Nachricht mit einem pre-shared key (PSK) signiert wird – in diesem Fall wird eine Hash-Funktion genutzt. In der bei OTTO genutzten Variante wird die Signatur dem ursprünglichen HTTP-Request als Signatur-Header beigefügt und daraufhin kann der Request vom Server entweder erlaubt oder verworfen werden.

Client

Um den Request zu signieren, muss der Client den Benutzernamen und den PSK mit dem aktuellen Zeitstempel und der eigentlichen Nachricht (also HTTP-Verb, URI, Body etc.) auf die richtige Weise kombinieren. Die Signatur wird dem Request ebenso als Header beigefügt wie der Zeitstempel. Da sich Client und Server den PSK teilen, wird er selbstverständlich nicht auch noch übertragen.
Das Diagramm zeigt, welche Informationen für die Signatur herangezogen werden. Als Beispiel wurde ein GET-Request auf die Ressource getLastOrder für den Benutzer example.user mit dem Geheimnis PSK am 22.06.2016 gegen halb zwölf mitteleuropäischer Zeit signiert.

HMAC

Server

Mit dem übertragenenen Benutzernamen kann der Server aus seiner Benutzerdatenbank den zugehörigen PSK ermitteln. Er wird genutzt, um mit den restlichen Informationen aus dem Request (Zeitstempel, HTTP-Verb etc.) dieselbe Signatur erneut zu generieren und sie mit der im Header übertragenen zu vergleichen. Unterscheiden sich die beiden Signaturen, dann weil die beim Signieren genutzten Information sich unterscheiden. Also entweder ist der PSK falsch oder es handelt sich um eine Replay-Attacke, bei der Teile des originalen Requests verändert wurden. Offensichtlich kann in beiden Fällen die Anfrage abgelehnt werden.

Andernfalls wird nun geprüft, ob der verifizierte Benutzer auch die Berechtigung hat, die gewünschte Aktion durchzuführen. Ist der Benutzer nicht dazu autorisiert, kann die Anfrage ebenfalls abgelehnt werden, ansonsten wird die Aktion durchgeführt.

Benutzung

Unter https://github.com/otto-de/hmac-auth findet sich eine Java-Implementierung dieser Methode für JVM-basierte Programme.

Um korrekte Header zu erzeugen, kann der HMACJerseyClient oder der HmacJersey2ClientRequestFilter benutzt werden, die bei der Instantiierung den Benutzernamen und den PSK bekommen. Als Zeitstempel wird die aktuelle Uhrzeit des Systems benutzt und die weiteren Informationen kommen aus dem Request, den der Client ja ohnehin generiert. Die Header werden dann ohne weiteres Zutun an jeden Request gehängt:

final WebTarget webTarget = jerseyRestClientFactory.getClient().target(url);
webTarget.register(new HmacJersey2ClientRequestFilter(userName, preSharedKey));

Um serverseitig die Header in Application-Containern zu prüfen, kann die andere Komponente der Bibliothek genutzt werden: der AuthenticationFilter prüft jeden ankommenden Request auf die HMAC-Header und führt eine Authentifizierung durch, falls sie vorhanden sind. Der Filter wird dem Application-Container einfach bekannt gemacht:

@Bean
public AuthenticationFilter authenticationFilter() {
    return new AuthenticationFilter(new AuthenticationService(new PropertyUserRepository()));
}

Ankommende Requests werden nun mit dem HTTP-Status 401 abgelehnt, wenn die HMAC-Header vorhanden sind, aber die Signatur falsch oder der Zeitstempel zu alt ist.

In Applikationen die Spring verwenden, steht für die Autorisierung die @AllowedForRoles-Annotation zur Verfügung, die beispielsweise an Service-Methoden gehängt werden kann. Ein Aspect unterbricht den Aufruf der Methode und prüft, ob der im Request angegebe Benutzer eine der Rollen hat, die in der Annotation stehen, bevor die Methode ausgeführt wird. Falls das nicht der Fall ist, wird eine AuthorizationException geworfen, die vom Benutzer der Bibliothek verarbeitet werden muss, die aber im Allgemeinen ebenfalls zu einem HTTP-Status 401 führt:

@RequestMapping(value = {"/priceOfIPhone"}, method = RequestMethod.PUT)
@AllowedForRoles(value = "iPhonePriceManager")
@ResponseBody
public void updateHealth(@RequestParam final Integer newIphonePrice) {
    final IPhoneData currentIPhoneData = allPhonesService.getIPhoneData();
    newIPhoneData = currentIPhoneData.withPrice(newIphonePrice);
    allPhonesService.setIPhoneData(newIPhoneData);
}

Eigenschaften der Implementierung

Sicher

Da die HMAC-Methode nicht vorschreibt, welche Hash-Algorithmen beim Signieren des Requests benutzt werden sollen, bleibt es der konkreten Implementierung vorbehalten, eine sichere Funktion zu wählen. Wir haben uns für HmacSHA256 entschieden, das genau für diese Zwecke entwickelt wurde und als State-of-the-Art Hash weit verbreitet und gut geprüft ist.

Andere denkbare Mechanismen wie z.B. HTTP-Basic sind so angreifbar, dass ihr Einsatz ohne zusätzliche SSL-Verschlüsselung, die wir uns ja ersparen wollen, grob fahrlässig ist.

Stateless

Die große Mehrzahl unserer Web-Server sind Docker-Container, die in ihrer Mesos-Umgebung ständig entstehen und vergehen können sollen, während die Maschinen je nach Last dynamisch skaliert werden. Bei zwei aufeinanderfolgenden Requests ist es also keinesfalls selbstverständlich, dass sie auf demselben Server landen. Auch für Blue-Green Deployments dürfen die Maschinen keinen Zustand teilen, der sich ändert.

Da unser Ansatz jeden Request erneut signiert, muss keine Session oder dergleichen im Server gehalten werden, die aufwändig zwischen den einzelnen Instanzen synchronisiert werden müsste.

Im Falle von HTTP-Digest, das eigentlich ein natürlicher Kandidat für die Problemstellung wäre, muss entweder die Nonce für jeden Benutzer in allen Servern synchronisiert oder der langsame initiale Handshake bei jeder Anfrage erneut ausgeführt werden.

Resilient gegenüber Replay-Angriffen

Bei diesen Angriffen wird versucht, zuvor abgehörte Requests erneut abzuschicken. Das ist Erfolg versprechend, weil der abgehörte Request ja vermutlich erfolgreich gewesen ist und die Angreiferin hoffen kann, dass auch der erneut gesendete Request erfolgreich sein wird. Diese Angriffe sind zwar theoretisch noch möglich, aber durch die zeitliche Begrenzung erheblich schwieriger – Requests haben einen eingebauten Verfallszeitpunkt.

Gelingt es Angreifern den Replay innerhalb der kurzen Gültigkeitsdauer durchzuführen, so sind die Angriffsmöglichkeiten für einzelne Requests recht eingeschränkt: ein POST ist sinnlos (eine neue Ressource erneut anlegen), ein PUT wird die durchgeführte Änderung an der Ressource erneut durchführen und sie damit zementieren, ein DELETE die bereits gelöschte Ressource erneut löschen und ein GET die nicht-vertraulichen Daten preisgeben. Lediglich bei direkt aufeinander folgenden Requests ist es denkbar, dass die Angreiferin genug Zeit hat, nach einem Request den vorigen erneut abzusetzen und so beispielsweise kürzlich gelöschte Ressourcen wieder herzustellen oder eine Konfiguration auf einen früheren Zeitpunkt zurückzudrehen. Allerdings sind unsere Business-Prozesse in dieser Hinsicht ohnehin sehr tolerant, da sie menschliche Fehler berücksichtigen müssen, daher könnte ein Angreifer im besten Falle hoffen, ein wenig Verwirrung zu stiften.

Eine Variante davon ist, den ursprünglichen Request zu verändern. So könnte eine Angreiferin beispielsweise versuchen einen GET zu einem DELETE Request zu ändern, um eine zuvor angefragte Resource zu löschen. Da allerdings sowohl das HTTP-Verb als auch der Request-Body Teil der Signatur sind, führen Manipulationen daran lediglich dazu, dass die Signatur nicht mehr zum Request passt und er abgelehnt wird.

Lediglich die unbeteiligten Request-Header bleiben Angreifern, um sie zu manipulieren und dort sind üblicherweise keine interessanten Daten enthalten.

Performance

Die Methode ist ausgesprochen schnell, insbesondere im Vergleich zu Methoden mit integrierter Verschlüsselung. Falls vertrauliche Daten übermittelt werden sollen, ist die Kombination von HMAC mit einer Transportverschlüsselung wie SSL immer noch erheblich schneller als Methoden im Application-Layer wie oAuth2 oder die verschiedenen wsSecurity-Varianten.

Einfach …

Security ist schwer, aber wir wollen trotzdem möglichst keine Bugs in diesem Bereich.
Insbesondere in unserem multilingualen Umfeld, in dem möglicherweise Teams eigene Implementierungen des Standards umsetzen müssen, ist es von großem Vorteil eine Methode zu nutzen, die mit wenigen Worten umfassend beschrieben werden und die insbesondere auch ohne fortgeschrittene mathematische Methoden umgesetzt werden kann

… aber nur für Maschinen

Durch die umständliche Berechnung der Header und die zeitliche Begrenzung der Requestgültigkeit ist es sehr schwer, manuell korrekte Requests zu generieren. Um dennoch Test-Requests an HMAC-gesicherte Schnittstellen senden zu können, gibt es im Github-Project einen Proxy, der lokal gestartet werden kann und der Requests entgegennimmt, die notwendigen Header erzeugt und den korrekten Request weiterleitet. Die Benutzung bleibt aber trotz allem recht sperrig und ist nicht für technisch unbedarfte Benutzer geeignet.