diff --git a/docs/details/webhooks.mdx b/docs/details/webhooks.mdx index 41fdb1bce4f8072162b467570634a06ae5d44814..a302cb730332282e45c51dc0bfe054ced066c7d6 100644 --- a/docs/details/webhooks.mdx +++ b/docs/details/webhooks.mdx @@ -1,10 +1,9 @@ --- -title: Sicherheitsvorgaben für Callbacks +title: Verwendung von Callbacks --- import ApiLink from '@site/src/components/ApiLink' - Der FIT-Connect Zustelldienst informiert sendende und empfangende Systeme (API-Clients) aktiv über [neue Einreichungen](../getting-started/receiving/query.mdx) oder [Statusupdates](../getting-started/sending/query-status.mdx). Hierzu werden HTTP-Callbacks genutzt, die auch als [Webhooks](https://de.wikipedia.org/wiki/WebHooks) bezeichnet werden. Webhooks ermöglichen es, API-Clients aktiv über diese Ereignisse zu informieren ohne dass eine regelmäßige Abfrage ([Polling](https://de.wikipedia.org/wiki/Polling_(Informatik))) nötig wäre. @@ -13,47 +12,130 @@ Technisch werden Webhooks als HTTP-POST-Request realisiert. Im Folgenden verwenden wir die Begriffe *Callback* und *Webhook* synonym. ## Callback-URL -Hierzu stellen API-Clients einen HTTP-Endpunkt bereit, an den der Zustelldienst einen HTTP-POST-Request mit übermitteln kann. +API-Clients stellen zum Empfang von Callbacks einen HTTP-Endpunkt bereit, an den der Zustelldienst einen HTTP-POST-Request übermitteln kann. +API-Clients müssen auf eingehende Callbacks mit einer HTTP-Response mit Status Code `200 OK` antworten. +Die URL dieses HTTP-Endpunkts bezeichnen wir als Callback-URL (`callbackUrl`). +Sie wird von dem an FIT-Connect angebundenen System festgelegt. -Die URL dieses HTTP-Endpunkts (`callbackUrl`) wird von dem an FIT-Connect angebundenen System festgelegt. Eine solche Callback-URL kann z.B. wie folgt aussehen: ``` https://fachverfahren.beispielstadt.example.org/callbacks/fit-connect ``` -Die Callback-URL **MUSS** über HTTPS erreichbar sein. +Die Callback-URL **DARF NUR** über HTTPS erreichbar sein. Der Zustelldienst wird Callbacks nur über eine via HTTPS verschlüsselte Verbindung auslösen. ## Konfiguration von Callbacks -TODO: Aushandlung des Callback Secret via API +Eine Konfiguration von Callbacks ist [über das Self-Service-Portal](../getting-started/receiving/destination.mdx) und über die API-Endpunkte <ApiLink to="/destinations/{destinationId}" withMethod="put" /> bzw. <ApiLink to="/destinations/{destinationId}" withMethod="patch" /> möglich. +Bei der Konfiguration werden die *Callback-URL* und ein *Callback-Secret* vom API-Client festgelegt. +Das *Callback-Secret* dient der Überprüfung der Echtheit (Authentizität) von eingehenden Callbacks (siehe nächster Abschnitt). +Das angegebene *Callback-Secret* kann über die API nur geschrieben und aktualisiert, aber nicht gelesen werden und **DARF NICHT** an Dritte weitergegeben werden. + +Ein sicheres *Callback-Secret* kann über die folgenden Aufrufe erzeugt werden: +- Python: `python -c 'import secrets; print(secrets.token_urlsafe(32))'` +- Ruby: `ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'` +- pwgen: `pwgen --secure 64 1` + +Die Einrichtung von Callbacks im Self-Service-Portal wird im Artikel `TODO: Link einfügen` näher beschrieben. + +### Konfiguration von Callbacks für Zustellpunkte +Über die API können Callbacks für Zustellpunkte wie folgt konfiguriert werden: +<Tabs +defaultValue="curl" +values={[ + {label: 'curl', value: 'curl',}, + {label: 'Java', value: 'java',}, + {label: 'JavaScript', value: 'javascript',}, +]}> +<TabItem value="curl" label="curl"> +```shell +$ SERVICE_URL=... +$ JWT_TOKEN=... +$ DESTINATION_ID=... +$ CALLBACK_URL=https://fachverfahren.beispielstadt.example.org/callbacks/fit-connect +$ CALLBACK_SECRET=insecure_unsafe_qHScgrg_kP-R31jHUwp3GkVkGJolvBchz65b74Lzue0 +$ curl -X PATCH \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $JWT_TOKEN" \ + --data "{\"callback\": {\"url\": \"$CALLBACK_URL\", \"secret\": \"$CALLBACK_SECRET\"}}" \ + "$SERVICE_URL/destinations/$DESTINATION_ID" +``` +</TabItem> +<TabItem value="java" label="Java"> +:::caution TODO +TODO: Codebeispiel ergänzen +::: +</TabItem> +<TabItem value="JavaScript" label="JavaScript"> +:::caution TODO +TODO: Codebeispiel ergänzen +::: +</TabItem> +</Tabs> ## Prüfung von Callbacks API-Clients, die Callbacks empfangen, **MÜSSEN** zwingend sicherstellen, dass ausgelöste Callbacks von einem vertrauenswürdigen Zustelldienst stammen. -Hierzu enthalten Callbacks einen Message Authentication Code (HMAC) gemäß [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) auf Basis eines geheimen symmetrischen Schlüssels. -Der geheime Schlüssel, im Folgenden *Callback Secret* genannt, wird bei der Konfiguration eines Callbacks festgelegt **DARF NICHT** an Dritte weitergegeben werden. +Hierzu enthalten Callbacks einen Message Authentication Code (HMAC) gemäß [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) auf Basis des angegebenen *Callback-Secrets*. +Ein Message Athentication Code kann als „symmetrische Signatur“ verstanden werden und ermöglicht die Prüfung der Herkunft und Integrität eines eingehende Callbacks. Der Message Authentication Code wird im HTTP-Header `callback-authentication` übertragen. Um [Replay-Angriffe](https://de.wikipedia.org/wiki/Replay-Angriff) zu vermeiden, enthält der Message Authentication Code einen aktuellen Timestamp. Dieser Timestamp wird im HTTP-Header `callback-timestamp` übertragen. - -Durch die Prüfung des Message Authentication Code können API-Clients die Herkunft und Integrität eines Callbacks verifizieren. +Bei der Prüfung der Echtheit des ausgelösten Callbacks **MÜSSEN** API-Clients prüfen, dass der angegebene Timestamp nicht älter als **5 Minuten** ist. Das folgende Beispiel zeigt die Verwendung der HTTP-Header `callback-authentication` und `callback-timestamp`: ```http POST /callbacks/fit-connect -callback-authentication: HMAC(key={shared-secret}, message={timestamp}.{http-body}) -callback-timestamp: 1631283222821 +callback-authentication: f4eig0ht6hdlsfz6DVqGjXi1j3RAombIQ7vjG1M2TFZx1fGurzg1nOEh00lPfLEulhio1RyTOav1e1aMi69SRg== +callback-timestamp: 1672527599 -{"submissionIds":["..."]} +{"type":"https://schema.fitko.de/fit-connect/callbacks/new-submissions","submissionIds":["f39ab143-d91a-474a-b69f-b00f1a1873c2"]} ``` -Der HMAC wird gebildet aus dem im HTTP-Header `callback-timestamp` übertragenen Zeitstempel und dem im HTTP-Body übertragenen Payload, getrennt durch das Zeichen `.` (Punkt). +Der HMAC wird gebildet aus dem im HTTP-Header `callback-timestamp` übertragenen Zeitstempel und dem im HTTP-Body übertragenen Payload, getrennt durch das Zeichen `.` (Punkt), jeweils UTF-8-kodiert. +Als Hash-Algorithmus wird SHA-512 verwendet. +``` +callback-authentication = BASE64(HMAC(key={callback-secret}, message={timestamp}.{http-body})) +``` Um den Message Authentication Code (HMAC) zu verifizieren, bildet der API-Client mit Hilfe des *Callback Secret* den HMAC nach und vergleicht diesen mit dem im HTTP-Header `callback-authentication` übertragenen HMAC. -Dabei sind die folgenden Implementierungshinweise zwingend zu beachten: -- Bei der Erzeugung des HMAC *MUSS* der Hash-Algorithmus `SHA-512` verwendet werden. -- Es *MUSS* geprüft werden, dass der angegebene Zeitstempel nicht älter als **5 Minuten** ist. -- Beim Vergleich des übertragenen HMAC und des vom API-Client gebildeteten HMAC *MUSS* ein zeitlich konstanter Zeichenfolgenvergleich (*constant time string comparison*) verwendet werden. In Python kann dies über die Verwendung der Methode `hmac.compare_digest` erreicht werden. +Bei der Prüfung **MÜSSEN** die folgenden Implementierungshinweise zwingend zu beachten: - Das Callback Secret **MUSS** in API-Clients konfigurierbar sein und **DARF NICHT** fest im Quellcode eines API-Clients einprogrammiert sein. -- Callbacks mit ungültigem Message Authentication Code **MÜSSEN** von API-Clients irgnoriert werden. + - Dies kann beispielsweise durch die Konfiguration des Callback Secret in einer Konfigurationsdatei oder über eine Umgebungsvariable (`$ export CALLBACK_SECRET=your_secret`) erreicht werden. +- Bei der Erzeugung des HMAC **MUSS** der Hash-Algorithmus `SHA-512` verwendet werden. +- Es **MUSS** geprüft werden, dass der angegebene Zeitstempel nicht älter als **5 Minuten** ist. +- Beim Vergleich des übertragenen HMAC und des vom API-Client gebildeteten HMAC **MUSS** ein zeitlich konstanter Zeichenfolgenvergleich (*constant time string comparison*) verwendet werden. + - In Python kann dies über die Verwendung der Methode [`hmac.compare_digest`](https://docs.python.org/2/library/hmac.html#hmac.compare_digest) erreicht werden. + - In Ruby kann dies über die Verwendung der Methode [`secure_compare`](https://rubydoc.info/github/rack/rack/master/Rack/Utils:secure_compare) erreicht werden. +- Callbacks mit ungültigem Message Authentication Code **MÜSSEN** von API-Clients ignoriert/verworfen werden. + +Dabei ist zunächst der Zeitstempel (`callback-timestamp`-Header) und anschließend der HMAC (`callback-authentication`-Header) zu prüfen: + +```python +# 1. Timestamp überprüfen +current_time_epoch = int(time.time()) +seconds_five_minutes = 60 * 5 +if current_time_epoch - request['headers']['callback-timestamp'] > seconds_five_minutes: + print('Error: timestamp too old') + sys.exit(1) +else: + print('timestamp ok') + +# 2. HMAC berechnen +payload = str(request['headers']['callback-timestamp']) + '.' + request['body'] + +expected_hmac = hmac.digest(CALLBACK_SECRET.encode("utf-8"), payload.encode("utf-8"), digest=sha512) +expected_hmac_base64 = base64.b64encode(expected_hmac).decode() + +print('hmac', expected_hmac_base64) + +# 3. Berechneten HMAC mit HMAC aus HTTP-Header vergleichen +if not hmac.compare_digest(request['headers']['callback-authentication'], expected_hmac_base64): + print('Error: invalid hmac') + sys.exit(2) +else: + print('hmac ok') +``` + +Das dargestellte Script findet sich auch zur freien Verwendung im [FIT-Connect-Tools-Repository](https://git.fitko.de/fit-connect/fit-connect-tools/-/blob/main/validate-callback.py).