Prüfung eines Security Event Token (SET)
Onlinedienste kontrollieren, ob der Zustelldienst eine Einreichung erhalten hat.
Dazu rufen sie vom Zustelldienst das Ereignisprotokoll zu dem Vorgang ab,
zu dem die Einreichung gehört.
In diesem Protokoll dokumentiert der Zustelldienst in Form
eines Security Event Token (SET), ob die Einreichung im Zustelldienst angekommen ist.
Im Folgenden ist beschrieben, in welcher Hinsicht Sie ein SET prüfen:
Wurde zur Signierung dieses SETs ein zulässiges Verfahren verwendet?
Wurden die kryptografischen Vorgaben eingehalten?
Stammt dieses SET tatsächlich vom Zustelldienst oder Empfänger?
Wenn Sie ein SDK von FIT-Connect verwenden, dann können Sie sich auf den Aufruf der Schnittstelle zum SDK konzentrieren, weil die SDKs von FIT-Connect viele Details automatisch intern umsetzen, wie auch das Prüfen von SETs. Sie finden eine Beschreibung der SDKs im Hauptmenü unter "SDKs".
Prüfung eines Security Event Tokens
Um eine vollständige Prüfung eines Security Event Tokens durchzuführen, MUSS zwingend sowohl die Einhaltung der (kryptografischen) Vorgaben als auch die Signatur geprüft werden.
Die Prüfung der Signatur des SET ist abhängig vom ausstellenden System (Zustelldienst, Verwaltungssystem oder Onlinedienst).
Aus technischer Sicht sollte auch das unter $schema
angegebene SET-Payload-Schema validiert werden.
Dies wird z. B. auch schon vom zuständigen Zustelldienst gemacht.
Aktuell ist die Angabe eines SET-Payload-Schemas aber noch optional und kann nicht existieren.
Dies wird in unspezifizierter Zukunft deprecated
und alle SETs müssen ein valides SET-Payload-Schema verwenden und angeben.
In dem Fall, dass das SET-Payload-Schema nicht angegeben ist, müssen trotzdem die kryptografischen Vorgaben bzw. die kryptografische Signatur validiert werden.
Sowohl die Prüfung der kryptografischen Vorgaben, als auch die Prüfung der kryptografischen Signatur darf KEINESFALLS ausgelassen werden.
Prüfung der Einhaltung von kryptografischen Vorgaben und der Struktur
Alle generierten Security Event Tokens MÜSSEN den Vorgaben aus RFC 7519 entsprechen und über folgende Header-Attribute verfügen:
Feld | Inhalt | Erläuterung |
---|---|---|
typ | secevent+jwt | Wird gemäß RFC 8417, Abschnitt 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 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. |
In der Payload des signierten SET MÜSSEN die folgenden standardisierten Felder gesetzt sein (Temporäre Ausnahme $schema
):
Feld | Inhalt | Erläuterung |
---|---|---|
$schema | URL Referenz auf das verwendete SET-Payload-Schema | Gibt das SET-Payload-Schema an, dem das SET entspricht. Dieses Schema wird auch vom Zustelldienst validiert. Das Schema kann zur client-seitigen Validierung der SET Payload von der referenzierten URL bezogen werden. Auch eine client-seitige Validierung durch statisch hinterlegte Schema-Versionen ist möglich, solange die von uns vorgegebenen Versionen unterstützt werden. Mit diesem Schema stellt auch der Zustelldienst seine SETs aus. |
jti | UUID des Token | Die JWT ID ist eine eindeutige ID des SET bzw. JWT. Es wird eine zufällige UUID verwendet. |
iss | ID des Token Issuers | Diese Angabe dient dazu, um herauszufinden, wer den Token ausgestellt hat. Für SETs, die vom Zustelldienst ausgestellt sind, wird die Host-Adresse (API-URL) verwendet. Bei SETs von empfangenden Systemen ist die destinationId , an der dieser die Submission schickt. |
iat | Timestamp (UNIX-Format) | Zeitpunkt der Ausstellung des SET. |
sub | URI, die den Gegenstand des SET identifiziert | Das Subject eines SET ist eine Kombination aus dem Schlüsselwort submission und der Id submissionId der Resource. |
txn | URI, die den Vorgang identifiziert | Als "Transaction Identifier" wird die Vorgangsreferenz caseId angegeben. |
events | JSON-Objekt der Events in diesem Event-Token | Das Objekt events enthält genau ein Ereignis zu einem logischen Sachverhalt bzw. Gesamtereignis, wie bspw. der Versendung einer Einreichung durch den Sender. Dieses Objekt beinhaltet immer zwingend eine URI, die das jeweilige Gesamtereignis eindeutig identifiziert. Das Objekt der URI des Gesamtereignisses kann leer sein oder weitergehende Informationen beinhalten (Siehe Ereignisse) |
{
"typ": "secevent+jwt",
"alg": "PS512",
"kid": "dd0409e5-410e-4d98-85b6-f81a40b8d980",
}
{
"$schema": "https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json",
"jti": "8538165b-9ce3-4097-871d-5b9581a3b4d9",
"iss": "40847c29-06aa-40e2-bf28-c29884c694c4",
"iat": 1622796532,
"sub": "submission:02bf1d9f-282d-4abf-810a-c4104baf0afe",
"txn": "case:452b5ee6-35df-441a-bd39-6141723cf914",
"events": {
"https://schema.fitko.de/fit-connect/events/accept-submission": {
"authenticationTags": {
"metadata": "XFBoMYUZodetZdvTiFvSkQ",
"data": "UCGiqJxhBI3IFVdPalHHvA",
"attachments": {
"0b799252-deb9-42b0-98d3-c50d24bbafe0": "rT99rwrBTbTI7IJM8fU3El",
"25abf553-0e53-43b9-a14a-1581b32a9ee5": "i7226HEB7IchCxNuh7lCiu",
"046a9fa5-bed6-494b-aab6-d41056c6db79": "d48LxeolRdtFF4nzQibeYO"
}
}
}
}
}
- .NET (SDK)
- Java (SDK)
Diese Funktionalität wird durch das .NET-SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode GetStatusForSubmissionAsync()
der Klasse Sender
automatisch mit abgedeckt:
sender.GetStatusForSubmissionAsync(sentSubmission);
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Codebeispiele - examples von FIT-Connect hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
Im folgenden Beispiel kann die allgemeine Struktur eines SET über folgenden Code validiert werden.
Außerdem wird der Schlüssel verifiziert.
Die Prüfungen bauen auf der Methode validateTrueOrElseThrow
auf. Die Methode prüft, ob ein Ausdruck der Wahrheit entspricht.
Diese kann im Fehlerfall die Fehler sammeln oder direkt eine Exception werfen.
public void validate(SignedJWT signedJWT, UUID caseId, FitConnectKeyLookup keyLookup) {
try {
validateHeader(signedJWT.getHeader());
validatePayload(signedJWT.getJWTClaimsSet());
String subject = payload.getStringClaim("sub");
String tokenSubmissionId = subject.substring(subject.indexOf(':') + 1);
validateTrueOrElseThrow(tokenSubmissionId.equalsIgnoreCase(submissionId), "The provided subject does not match with the submission.");
String txn = payload.getStringClaim("txn");
String transactionId = txn.substring(txn.indexOf(':') + 1);
validateTrueOrElseThrow(transactionId.equalsIgnoreCase(caseId), "The provided txn does not match with the case.");
UUID jti = UUID.fromString(payload.getStringClaim("jti"));
try {
// Validate public Key
RSAKey parsedPublicKey = RSAKey.parse(publicKey);
validateRSAKey(parsedPublicKey, false);
JWSVerifier jwsVerifier = new RSASSAVerifier(parsedPublicKey);
validateTrueOrElseThrow(signedJWT.verify(jwsVerifier), "The signature of the token could not be verified with the specified key.");
} catch (ParseException | JOSEException e) {
throw new RuntimeException("The SET could not get parsed properly.");
}
} catch (AssertionError e) {
throw new RuntimeException(e.getMessage());
}
}
public static void validateRSAKey(RSAKey RSAKey, boolean isPrivate){
validateTrueOrElseThrow(RSAKey.getModulus().decodeToBigInteger().bitLength() >= 4096, "JWK has wrong key length.");
validateTrueOrElseThrow(RSAKey.getAlgorithm().equals(JWSAlgorithm.PS512), "The specified public key does not use PS512 as algorithm.");
if(isPrivate){
validateTrueOrElseThrow(RSAKey.getKeyOperations().size() == 1 &&
RSAKey.getKeyOperations().contains(KeyOperation.SIGN),
"The specified private key is not intended for 'sign' as specified through key operation.")
}
else{
validateTrueOrElseThrow(RSAKey.getKeyOperations().size() == 1 &&
RSAKey.getKeyOperations().contains(KeyOperation.VERIFY),
"The specified public key is not intended for 'verify' as specified through key operation.")
};
validateTrueOrElseThrow(RSAKey.getPublicExponent().toString().equals("AQAB"), "The specified key does not match the public exponent 'AQAB'.");
}
private static void validateTrueOrElseThrow(boolean expression, String msg) {
if (!expression) {
throw new RuntimeException(msg);
}
}
Der folgende Quellcode prüft, ob der Header Pflichtangaben enthält:
private void validateHeader(JWSHeader header) {
assertTrue(header.getAlgorithm() == JWSAlgorithm.PS512, "The provided alg in the SET header is not allowed.");
assertTrue(header.getType().toString().equals(FitConnectSET.HEADER_TYPE), "The provided typ in the SET header is not " + FitConnectSET.HEADER_TYPE);
assertTrue(header.getKeyID() != null, "The kid the SET was signed with is not set.");
}
Die Struktur des Payloads wird geprüft.
private void validatePayload(JWTClaimsSet payload) throws ParseException {
assertTrue(payload.getClaim("iss") != null, "The claim iss is missing in the payload of the SET.");
assertTrue(payload.getClaim("iat") != null, "The claim iat is missing in the payload of the SET.");
assertTrue(payload.getClaim("jti") != null, "The claim jti is missing in the payload of the SET.");
assertTrue(payload.getClaim("sub") != null, "The claim sub is missing in the payload of the SET.");
assertTrue(payload.getClaim("txn") != null, "The claim txn is missing in the payload of the SET.");
assertTrue(payload.getClaim("events") != null, "The claim events is missing in the payload of the SET.");
assertTrue(payload.getJSONObjectClaim("events").keySet().size() == 1, "Only exactly one event is allowed.");
// This matches exactly a UUIDv4 (https://stackoverflow.com/questions/136505/searching-for-uuids-in-text-with-regex)
String uuidPattern = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}";
String subject = payload.getStringClaim("sub");
assertTrue(subject.matches("(submission|case|reply):" + uuidPattern), "The provided subject does not match the allowed pattern.");
String transactionId = payload.getStringClaim("txn");
assertTrue(transactionId.matches("case:" + uuidPattern), "The provided txn does not match the allowed pattern.");
Optional<String> events = payload.getJSONObjectClaim("events").keySet().stream().findFirst();
events.ifPresentOrElse(
s -> assertTrue(FitConnectEvent.ofURL(s) != null, "The provided event is not a valid event supported by this instance."),
() -> fail("No events in JWT"));
}
Der Subject (sub
) Claim wird noch mal separat geprüft.
private void validateSubject(JWTClaimsSet payload) throws ParseException {
String subject = payload.getStringClaim("sub");
String tokenType = subject.substring(0, subject.indexOf(':'));
assertTrue(tokenType.equalsIgnoreCase("submission"), "The provided subject does not contain a submission. 'case' and 'reply' are not yet supported.");
}
Der Claim txn
muss mit der übergebenen caseId
übereinstimmen.
private void validateTxnClaim(JWTClaimsSet payload, UUID caseId) throws ParseException {
String txn = payload.getStringClaim("txn");
String transactionId = txn.substring(txn.indexOf(':') + 1);
assertTrue(transactionId.equalsIgnoreCase(caseId.toString()), "The provided txn does not match with the case.");
}
Die Signatur wird validiert.
Das Laden des Verifikations-Schlüssels ist hier in in den FitConnectKeyLookup
ausgelagert.
Details dazu im Folgenden.
private void validateSignature(SignedJWT signedJWT, FitConnectKeyLookup keyLookup) {
try {
final JWSHeader header = signedJWT.getHeader();
final String keyID = header.getKeyID();
final RSAKey rsaKey = keyLookup.getKey(keyID);
final JWSVerifier jwsVerifier = new RSASSAVerifier(rsaKey);
assertTrue(signedJWT.verify(jwsVerifier), "The signature of the token could not be verified with the specified key.");
} catch (ParseException | JOSEException e) {
fail("The SET could not get parsed properly.");
}
}
Signaturprüfung eines vom Zustelldienst ausgestellten SET
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 Set (JWKS) öffentlich zugänglich über den Endpunkt GET /.well-known/jwks.json
bereit.
Ein Beispiel für ein JWKS ist in folgendem Ausschnitt dargestellt:
{
"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.
Nach Abschluss der Prüfung aller kryptografischen Vorgaben kann mit diesem und einer entsprechenden Bibliothek eine Signaturprüfung durchgeführt werden.
- .NET (SDK)
- Java (SDK)
Diese Funktionalität wird durch das .NET-SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode GetStatusForSubmissionAsync()
der Klasse Sender
automatisch mit abgedeckt:
sender.GetStatusForSubmissionAsync(sentSubmission);
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Examples von FIT-Connect hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
Im folgenden Beispiel wird die Bibliothek nimbus-jose-jwt für die Prüfung genutzt.
static final SUBMISSION_API = "https://submission-api-testing.fit-connect.fitko.dev";
boolean verifyZustelldienstSignature(SignedJWT securityEventToken, String keyId) {
JWKSet jwks = JWKSet.load(SUBMISSION_API + "/.well-known/jwks.json");
JWK publicKey = jwks.getKeyByKeyId(keyId)
if (publicKey.getAlgorithm() != JWSAlgorithm.PS512) {
throw new RuntimeException("The key specified for signature verification doesn't use/specify PS512 as algorithm.")
}
JWSVerifier jwsVerifier = new RSASSAVerifier(publicKey.toRSAKey());
return securityEventToken.verify(jwsVerifier);
}
In der Stage- und Produktivumgebung ist das vom Zustelldienst für die Signatur der SET genutzte Schlüsselpaar über ein Zertifikat aus der Verwaltungs-PKI abgesichert (Attribut x5c
des JWK).
Für eine vollständige Prüfung der Authentizität der augestellen SET SOLLTE daher – analog zur Prüfung der öffentlichen Schlüssel im Rahmen der Verschlüsselung – eine Zertifikatsprüfung durchgeführt werden.
Signaturprüfung eines vom empfangenden System ausgestellten SET
Um die Signatur eines von einem empfangenden System ausgestellen SET zu überprüfen ist es notwendig, auf den verwendeten Schlüssel zugreifen zu können. Der bzw. die Schlüssel sind öffentlich verfügbar und können über die Submission API abgerufen werden.
Ausgangslage: Das SET
Als Ausgangslage dient das folgende SET.
Aus dem Header wird die Schlüssel-ID aus dem Feld kid
benötigt.
Aus dem Payload benötigen wir das Feld submissionId
.
Konkret sind das hier:
- kid:
dd0409e5-410e-4d98-85b6-f81a40b8d980
- submissionId:
F65FEAB2-4883-4DFF-85FB-169448545D9F
{
"typ": "secevent+jwt",
"alg": "PS512",
"kid": "dd0409e5-410e-4d98-85b6-f81a40b8d980",
}
{
"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"
}
Abruf des JWK zur Gültigkeitsprüfung des SET
Mit der submissionId
kann über den Endpunkt GET /v1/submissions/{submissionId}
die zugehörige destinationId
ermittelt werden.
Hier ist das konkret der Wert 92f2f581-c89d-44a5-b834-1fe3f6fa48d5
. Dabei können nur Submissions im Status Submitted oder Forwarded abgerufen werden.
GET /v1/submissions/F65FEAB2-4883-4DFF-85FB-169448545D9F
{
"destinationId": "92f2f581-c89d-44a5-b834-1fe3f6fa48d5",
// ...
}
Mit den zwei Informationen kid
und destinationid
kann nun der JWK zur Signaturprüfung abgerufen werden:
$ KID=...
$ SUBMISSION_API=https://submission-api-testing.fit-connect.fitko.dev
$ DESTINATION_ID=...
$ curl -X GET \
"$SUBMISSION_API/v1/destinations/$DESTINATION_ID/keys/$KID"
---
{
"kty": "RSA",
"e": "AQAB",
"keyops": ["verify"],
"x5c": [
"LS0tLS1CRUdJTiBDRVJU...jN1NGKzQKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
],
"x5t": "MTg6NTU6RUY6ME...MEM6QzM6ODQ6QjA6MkE6RkMK",
"kid": "787f3a1c-7da7-44d7-9b79-9783b1ea9be8",
"alg": "PS512",
"n": "sX2DX7rG5BoJd23...FlxHZt8T6ZqjRa1QcFnkq3_M4-tk"
}
Validierung des SET mit Hilfe des JWK
Die Verifikation des SET mit dem eben abgerufenen JWK ist – nach Abschluss der Prüfung aller kryptografischen Vorgaben – ziemlich geradlinig. Es wird zunächst geprüft, ob der Schlüssel den passenden Algorithmus hat. Anschließend wird die eigentliche Verifikation durch die Bibliothek durchgeführt.
- .NET (SDK)
- Java (SDK)
Diese Funktionalität wird durch das .NET-SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode GetStatusForSubmissionAsync()
der Klasse Sender
automatisch mit abgedeckt:
sender.GetStatusForSubmissionAsync(sentSubmission);
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Examples von FIT-Connect hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
boolean verifyClientSignature(SignedJWT securityEventToken, String keyId) {
JWK publicKey = getKeyForSET(securityEventToken, keyId);
if (publicKey.getAlgorithm() != JWSAlgorithm.PS512) {
throw new RuntimeException("The key specified for signature verification doesn't use/specify PS512 as algorithm.")
}
JWSVerifier jwsVerifier = new RSASSAVerifier(publicKey.toRSAKey());
return securityEventToken.verify(jwsVerifier);
}
In der Stage- und Produktivumgebung ist das von empfangenden Systemen (Subscriber)
für die Signatur der SET genutzte Schlüsselpaar über ein Zertifikat aus der Verwaltungs-PKI abgesichert (Attribut x5c
des JWK).
Für eine vollständige Prüfung der Authentizität der ausgestellten SET SOLLTE daher –
analog zur Prüfung der öffentlichen Schlüssel im Rahmen der Verschlüsselung – eine Zertifikatsprüfung durchgeführt werden.