diff --git a/docs/details/callbacks.mdx b/docs/details/callbacks.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e9bac650c9c247e79b199450e7a7fcf382997cf7 --- /dev/null +++ b/docs/details/callbacks.mdx @@ -0,0 +1,143 @@ +--- +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. +Technisch werden Webhooks als HTTP-POST-Request realisiert. + +Im Folgenden verwenden wir die Begriffe *Callback* und *Webhook* synonym. + +## Callback-URL +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. + +Eine solche Callback-URL kann z.B. wie folgt aussehen: +``` +https://fachverfahren.beispielstadt.example.org/callbacks/fit-connect +``` + +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 +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"> + +```bash +$ 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 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. +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: f4eig0ht6hdlsfz6DVqGjXi1j3RAombIQ7vjG1M2TFZx1fGurzg1nOEh00lPfLEulhio1RyTOav1e1aMi69SRg== +callback-timestamp: 1672527599 + +{"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), 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. + +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. + - 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). diff --git a/docs/getting-started/receiving/query.mdx b/docs/getting-started/receiving/query.mdx index dc184a6a18f763453dd8373c94b4533b9a87ba78..b56c19fb4c46f045c0303a12fa3845613d098092 100644 --- a/docs/getting-started/receiving/query.mdx +++ b/docs/getting-started/receiving/query.mdx @@ -5,31 +5,27 @@ title: Vorhandensein neuer Einreichungen prüfen import ApiLink from '@site/src/components/ApiLink' -Neue Einreichungen können über zwei Wege erhalten werden. Entweder können neue Einreichungen über Polling abgefragt werden oder der Zustellpunkt wird über einen Callback vom Zustelldienst benachrichtigt. -Letzteres wäre die präferierte Variante, da dadurch unnötige Anfragen vermieden werden können und die Kommunikation so auf ein notwendiges Mindestmaß reduziert wäre. +Empfangende Systeme können sich über den Eingang neuer Einreichungen entweder durch periodische Anfragen informieren (Polling) oder vom Zustelldienst über Callbacks aktiv benachrichtigen lassen. +Letzteres stellt die empfohle Umsetzungsvariante dar, da hierdurch unnötige Anfragen vermieden werden können, was zu einer insgesamt besseren Performance und zu einer schnelleren Benachrichtung empfangnder Systeme führt. ## Callback +Wenn eine Callback-URL im Zustellpunkt hinterlegt ist, kann der Zustelldienst den Zustellpunkt aktiv via Callback über neue Einriechungen informieren. +Das Format, in welchem der Callback übertragen wird, ist im Endpunkt <ApiLink to="/destinations/{destinationId}" withMethod="put" /> definiert. +Die Konfiguration und Prüfung von Callbacks ist im Aritkel [Verwendung von Callbacks](../../details/callbacks.mdx) beschrieben. -Wenn eine neue Einreichung im Zustelldienst bereitsteht und eine Callback-URL im Zustellpunkt hinterlegt ist, kann der Zustelldienst den Zustellpunkt direkt informieren. -Das Format, in welchem der Callback übertragen wird, im Endpunkt <ApiLink to="/destinations" withMethod="post" /> beschrieben. -Im Endeffekt, ist es jedoch ein Array mit UUIDs für jede bereitstehende Einreichung, wie im Beispiel unten. +Callbacks zur Benachrichtigung über neue Einriechungen haben das folgende Format: -```json -[ - "17b64151-fc80-4000-8fb7-6ddd08115780" -] -``` - -:::caution Hinweis -Der Mechanismus zum Konfigurieren und Entgegennehmen von Callbacks wird sich bis zur Veröffentlichung der finalen API-Spezifikation noch einmal ändern. -::: +```http +POST /callbacks/fit-connect +callback-authentication: f4eig0ht6hdlsfz6DVqGjXi1j3RAombIQ7vjG1M2TFZx1fGurzg1nOEh00lPfLEulhio1RyTOav1e1aMi69SRg== +callback-timestamp: 1672527599 +{"type":"https://schema.fitko.de/fit-connect/callbacks/new-submissions","submissionIds":["f39ab143-d91a-474a-b69f-b00f1a1873c2"]} +``` -Wenn der Zustellpunkt benachrichtigt wird, dann kann dieser über mehrere Befehle die Einreichung mit allen verbundenen Ressourcen herunterladen. -Mehrere Befehle dahingehend, da erst alle allgemeinen Informationen zur Einreichung heruntergeladen werden müssen und dann, falls notwendig alle mit ihr verbundenen Anlagen. +Wenn das empfangende System über den Eingang neuer Einreichungen benachrichtigt wurde, dann kann es diese wie im Artikel [Einreichung herunterladen](./download-submission.mdx) beschrieben herunterladen. ## Polling - Über Polling, d.h. regelmäßiges Abfragen des Endpuntes <ApiLink to="/submissions" /> , können alle Einreichungen abgefragt werden, die zum Abholen bereitstehen. Dies wird aus Performancegründen jedoch nicht empfohlen. diff --git a/docs/sidebar.js b/docs/sidebar.js index 7701752a3f6d6ea75bc2ec9d83de218457c7b707..799822474902f8180991998de24fa7e400de0b21 100644 --- a/docs/sidebar.js +++ b/docs/sidebar.js @@ -79,6 +79,7 @@ module.exports = { 'details/crypto', 'details/destination-management', 'details/jwk-creation', + 'details/callbacks', 'details/schema-reference', 'details/pgp-export', {