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