Skip to content
Snippets Groups Projects

docs(event-log): Start documenting the event log and interaction with signed JWTs

Merged David Schwarzmann requested to merge feat/security-event-tokens into main
Files
2
+ 216
0
---
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
Loading