From 9cc5824c33deabcca7f85ac525b7d8557ffc1f4f Mon Sep 17 00:00:00 2001
From: David Schwarzmann <david.schwarzmann@codecentric.de>
Date: Tue, 27 Jul 2021 11:16:59 +0200
Subject: [PATCH] docs(event-log): Start documenting the event log and
 interaction with signed JWTs

---
 docs/getting-started/event-log.md | 216 ++++++++++++++++++++++++++++++
 docs/sidebar.js                   |   1 +
 2 files changed, 217 insertions(+)
 create mode 100644 docs/getting-started/event-log.md

diff --git a/docs/getting-started/event-log.md b/docs/getting-started/event-log.md
new file mode 100644
index 000000000..6bdb1ddec
--- /dev/null
+++ b/docs/getting-started/event-log.md
@@ -0,0 +1,216 @@
+---
+title: Event Log
+---
+
+Im Ereignisprotokoll (Event Log) werden relevante Ereignisse (events) aufgezeichnet. Beim Abruf des Ereignisprotokolls
+liefert die API ein Array von JSON Web Token (JWT) gemäß [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519). Der
+JWT ist einen Security-Event-Token (SET) gemäß [RFC 8417](https://datatracker.ietf.org/doc/html/rfc8417). Wie mit den
+Security-Event-Token (SET) umgegangen wird, wird in diesem Abschnitt beschrieben.
+
+## :construction: Erstellung eines Security-Event-Token (SET)
+
+```java
+private static final InputStream jwksPath = GenerateSignedToken.class.getClassLoader().getResourceAsStream("jwks.json");;
+private static final UUID signatureKeyId = UUID.fromString("6508dbcd-ab3b-4edb-a42b-37bc69f38fed");
+
+private static final String subject = "submission:f65feab2-4883-4dff-85fb-169448545d9f";
+private static final String event = "https://schema.fitko.de/fit-connect/events/accept-submission";
+private static final String transactionId = "case:f73d30c6-8894-4444-8687-00ae756fea90";
+// …
+try {
+    JWKSet localKeys = JWKSet.load(jwksPath);
+    JWK key = localKeys.getKeyByKeyId(signatureKeyId.toString());
+
+    if (key == null) {
+        throw new RuntimeException("Cannot find key with specified Key Id");
+    }
+
+    JWSSigner signer = new RSASSASigner(key.toRSAKey());
+    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+            .issuer("https://my-custom-identifyable-service.domain")
+            .issueTime(new Date())
+            .jwtID(UUID.randomUUID().toString())
+            .subject(subject)
+            .claim("events", Map.of(event, Map.of()))
+            .claim("txn", transactionId)
+            .build();
+
+    JWSHeader header = JWSHeader.parse(Map.of(
+            "typ", "secevent+jwt",
+            "kid", key.getKeyID(),
+            "alg", "PS512"
+    ));
+
+    SignedJWT signedJWT = new SignedJWT(
+            header,
+            claimsSet);
+
+    signedJWT.sign(signer);
+
+    signedJWT.serialize(); // => SET, serialized as Base64 encoded string
+} catch (IOException | ParseException e) {
+    throw new RuntimeException("Error during loading of JWK-Set with signature keys.");
+} catch (JOSEException e) {
+    throw new RuntimeException("Could not generate SET");
+}
+```
+
+## Prüfung eines Security-Event-Token (SET)
+
+### Allgemeine Struktur
+
+Alle generierten Security Event Tokens MÃœSSEN den Vogaben aus [RFC 7519](https://tools.ietf.org/html/rfc7519)
+entsprechen und über folgende Header-Attribute verfügen:
+
+| Feld  | Inhalt         | Erläuterung |
+| ----- | -------------- | ----------- |
+| `typ` | `secevent+jwt` | Wird gemäß [RFC 8417, Abschnitt 2.3](https://datatracker.ietf.org/doc/html/rfc8417#section-2.3) auf den festen Wert "`secevent+jwt`" gesetzt. |
+| `alg` | `PS512` | Zur Signaturerstellung wird der Signaturalgorithmus RSASSA-PSS mit SHA-512 und MGF1 mit SHA-512 verwendet. Vorgabe gemäß [BSI TR-02102](https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02102/BSI-TR-02102.html) in der Version 2021-01 (Stand 24. März 2021). |
+| `kid` | *Key-ID des zugehörigen Public
+Keys* | Die Key-ID des Public Key, mit dem die Signatur des JWT geprüft werden kann. |
+
+Im Payload des signierten SET MÃœSSEN mindestens die
+folgenden [standartisierten Felder](https://www.iana.org/assignments/jwt/jwt.xhtml) gesetzt sein:
+
+| Feld   | Inhalt                                    | Erläuterung                                                  |
+| ------ | ----------------------------------------- | ------------------------------------------------------------ |
+| iss    | Identifizierungsmerkmal des Token Issuers | Dient dazu, um herauszufinden, wer den Token ausgestellt hat. Für vom Zustelldienst ausgestellt SET wird die Basis-Adresse (API-URL) verwendet. Für die antragsendenden und -empfangenden Systeme wird empfohlen, auch eine dem System zugeordnete URL zu verwenden. Sofern das System keine zur Identifikation nutzbare URL hat, ist eine andere eindeutige URI zu verwenden. |
+| iat    | Timestamp (UNIX-Format)                   | Zeitpunkt der Ausstellung des SET.                            |
+| jti    | UUID des Token                            | Die JWT ID ist eine eindeutige ID des SET bzw. JWT. Es wird eine zufällige UUID verwendet. |
+| sub    | URI, die den Gegenstand des SET identifiziert. | Das Subject eines SWT ist entweder eine Ãœbertragung (submission), eine Antwort (reply) oder eine Vorgangsreferenz (case id). Die Angabe besteht jeweils aus Typ und ID (UUID) der Resource. |
+| events | Dict der Events in diesem Event-Token     | Das Objekt "events" enthält immer genau ein Event. Das Event selbst ist wieder ein Objekt, welches derzeit immer leer ist. Events könnten in Zukunft um Zusatzinformationen ergänzt werden. |
+| txn    | URI, die den Vorgang identifiziert        | Als "Transaction Identifier" wird die Vorgangsreferenz angegeben, auch wenn das Subject selbst die Vorgangsreferenz ist. In diesem Fall sind Subject und Transaction Identifier gleich. |
+
+:::note SET Beispiel
+
+```json title="SET Header"
+{
+  "typ": "secevent+jwt"
+  "alg": "PS512",
+  "kid": "dd0409e5-410e-4d98-85b6-f81a40b8d980",
+}
+```
+
+```json title="SET Payload"
+{
+  "iss": "https://api.fitko.de/fit-connect/",
+  "iat": 1622796532,
+  "jti": "0BF6DBF6-CE7E-44A3-889F-82FE74C3E715",
+  "sub": "submission:F65FEAB2-4883-4DFF-85FB-169448545D9F",
+  "events": {
+    "https://schema.fitko.de/fit-connect/events/accept-submission": {}
+  },
+  "txn": "case:F73D30C6-8894-4444-8687-00AE756FEA90"
+}
+```
+
+:::
+
+Im folgenden Beispiel kann die allgemeine Struktur eines SET über folgenden Code validiert werden.
+
+```java
+SignedJWT securityEventToken = SignedJWT.parse(eventToken);
+JWTClaimsSet payload = securityEventToken.getJWTClaimsSet();
+UUID keyId = UUID.fromString(securityEventToken.getHeader().getKeyID());
+
+validateTokenStructure(securityEventToken);
+
+verifySignature(securityEventToken, keyId); // Abhängig von der Quelle
+```
+
+```java
+boolean validateTokenStructure(SignedJWT securityEventToken) {
+  try {
+    validateHeader(signedJWT.getHeader());
+    validatePayload(signedJWT.getJWTClaimsSet());
+  } catch (ParseException e) {
+    throw new SecurityEventTokenValidationException("The payload of the SET could not get parsed properly.");
+  }
+}
+
+private void validateHeader(JWSHeader header) {
+  validateTrueOrElseThrow(header.getAlgorithm() == JWSAlgorithm.PS512, "The provided alg in the SET header is not allowed.");
+  validateTrueOrElseThrow(header.getType().toString().equals("secevent+jwt"), "The provided typ in the SET header is not secevent+jwt");
+  validateTrueOrElseThrow(header.getKeyID() != null, "The kid the SET was signed with is not set.");
+}
+
+private void validatePayload(JWTClaimsSet payload) throws ParseException {
+  validateTrueOrElseThrow(payload.getClaim("iss") != null, "The claim iss is missing in the payload of th SET.");
+  validateTrueOrElseThrow(payload.getClaim("iat") != null, "The claim iat is missing in the payload of th SET.");
+  validateTrueOrElseThrow(payload.getClaim("jti") != null, "The claim jti is missing in the payload of th SET.");
+  validateTrueOrElseThrow(payload.getClaim("sub") != null, "The claim sub is missing in the payload of th SET.");
+  validateTrueOrElseThrow(payload.getClaim("txn") != null, "The claim txn is missing in the payload of the SET.");
+  validateTrueOrElseThrow(payload.getClaim("events") != null, "The claim events is missing in the payload of the SET.");
+  validateTrueOrElseThrow(payload.getJSONObjectClaim("events").keySet().size() == 1, "Only exactly one event is allowed.");
+
+  String uuidPattern = "\\b[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\\b[0-9a-fA-F]{12}\\b";
+
+  String subject = payload.getStringClaim("sub");
+  validateTrueOrElseThrow(subject.matches("(submission|case|reply):" + uuidPattern), "The provided subject does not match the allowed pattern.");
+
+  String transactionId = payload.getStringClaim("txn");
+  validateTrueOrElseThrow(transactionId.matches("case:" + uuidPattern), "The provided txn does not match the allowed pattern.");
+
+  String event = payload.getJSONObjectClaim("events").keySet().stream().findFirst().get();
+  validateTrueOrElseThrow(Event.ofURL(event) != null, "The provided event is not a valid event supported by this instance.");
+}
+
+private void validateTrueOrElseThrow(boolean expression, String msg) {
+  if (!expression) {
+    throw new SecurityEventTokenValidationException(msg);
+  }
+}
+```
+
+### … des Zustelldienstes
+
+Um die Signatur eines SET zu überprüfen, welches vom Zustelldienst ausgestellt wurde, ist es notwendig auf die
+verwendeten Schlüssel zugreifen zu können. Der Zustelldienst stellt ein JSON Web Key (JWK) Set öffentlich zugänglich
+unter der URL `/.well-known/jwks.json` bereit. Ein Beispiel für ein JWK Set ist in folgendem Ausschnitt dargestellt:
+
+```json
+{
+  "keys": [
+    {
+      "alg": "PS512",
+      "e": "AQAB",
+      "key_ops": [
+        "verify"
+      ],
+      "kid": "6508dbcd-ab3b-4edb-a42b-37bc69f38fed",
+      "kty": "RSA",
+      "n": "65rmDz943SDKYWt8KhmaU…ga16_y9bAdoQJZRpcRr3_v9Q"
+    },
+    {
+      "alg": "PS512",
+      "e": "AQAB",
+      "key_ops": [
+        "verify"
+      ],
+      "kid": "14a70431-01e6-4d67-867d-d678a3686f4b",
+      "kty": "RSA",
+      "n": "wnqKgmQHSqJhvCfdUWWyi8q…yVv3TrQVvGtsjrJVjvJR-s_D7rWoBcJVM"
+    }
+  ]
+}
+```
+
+Mit diesem JWK Set kann die Signatur eines Security-Event-Tokens überprüft werden. Hierfür muss der Schlüssel mit der
+passenden `kid` aus dem Header des SET’s im JWK Set gesucht werden. Dann kann man mit diesem und einer entsprechenden
+Bibliothek eine Signaturprüfung durchführen. Im folgenden Beispiel wird die
+Bibliothek [nimbus-jose-jwt](https://connect2id.com/products/nimbus-jose-jwt) für die Prüfung genutzt.
+
+```java
+static final ZUSTELLDIENST_BASE_URL = "https://zustelldienst.example.com";
+
+boolean verifySignature(SignedJWT securityEventToken, String keyId) {
+  JWKSet jwks = JWKSet.load(ZUSTELLDIENST_BASE_URL + "/.well-known/jwks.json");
+  JWK publicKey = jwks.getKeyByKeyId(keyId)
+  JWSVerifier jwsVerifier = new RSASSAVerifier(publicKey.toRSAKey());
+  return signedJWT.verify(jwsVerifier);
+}
+```
+
+### :construction: … eines Senders/Empfängers
+
+TBD
diff --git a/docs/sidebar.js b/docs/sidebar.js
index 44fc1e2ca..0d2693c40 100644
--- a/docs/sidebar.js
+++ b/docs/sidebar.js
@@ -29,6 +29,7 @@ module.exports = {
         'getting-started/authentication',
         'getting-started/metadata',
         'getting-started/encryption',
+        'getting-started/event-log',
         {
           type: 'category',
           label: 'Versenden',
-- 
GitLab