Zustellpunkt ermitteln
Zustellpunkt und destinationId
über die Routing API ermitteln
Um eine Einreichung an die fachlich korrekte Stelle sicherzustellen und die technischen Parameter des richtigen Zustellpunkts zu ermitteln, muss die Destination-ID der zuständigen Stelle (destinationId
) und die Adresse des zuständigen Zustelldienstes (submissionUrl
) ermittelt werden.
Der Abruf von Routing-Informationen über die Routing-API ist in der produktiven Umgebung bereits jetzt möglich. Hierzu müssen die Zuständigkeitsinformationen zuvor in den Landesredaktionen konfiguriert werden. Übergangsweise kann die Destination-ID zu Testzwecken auch manuell im sendenden System hinterlegt werden.
Die über das Self-Service-Portal erstellten Zustellpunkte sind in der Testumgebung ebenfalls nicht automatisch über die Routing-API auffindbar.
Dieses Feature ist zur Erleichterung von Anbindungstests als zukünftige Erweiterung geplant.
Für eine Auffindbarkeit der Zustellpunkte über die Routing-API der Testumgebung ist derzeit eine manuelle Pflege der Zuständigkeitsinformationen in der Demo-Umgebung des Portalverbund Onlinegateway (PVOG) notwendig. Für die manuelle Eingabe sind Destination-ID
, Adressierungsinformationen
(destinationSignature
), ARS
und Leika
notwendig. Diese können aus dem Zustellpunkt im Self-Service-Portal ausgelesen werden. Kontaktieren Sie mit diesen Daten oder für weitere Informationen unser Anbindungsmanagement (fit-connect@spotgroup.de).
Sofern eine Destination-ID bereits bekannt ist, können die in einem Zustellpunkt hinterlegten technischen Parameter alternativ auch über den Endpunkt GET /v1/destinations/{destinationId}
der Submission API des zuständigen Zustelldienstes abgerufen werden (siehe unten).
Die Ermittlung der destinationId
und die Ermittlung der technischen Parameter über die Routing-API erfolgt über einen GET-Request auf den Endpunkt GET /routes
des FIT-Connect Routingdienstes.
Der Endpunkt erwartet genau zwei Parameter:
- Einen Identifikator einer Verwaltungsleistung. Als Identifikator der Verwaltungsleistung muss ein Leistungsschlüssel aus dem FIM-Baustein Leistungen (ehemals LeiKa-Schlüssel, siehe Leistungskatalog im FIM-Portal) verwendet werden.
- Einen Identifikator eines verwaltungspolitischen Gebietes. Für den Identifikator des verwaltungspolitischen Gebietes kann entweder der amtliche Gemeindeschlüssel (AGS), der amtliche Regionalschlüssel (ARS) oder die Id eines Gebietes aus der Suche über den Endpunkt
GET /areas
verwendet werden.
Der Endpunkt GET /routes
implementiert Pagination.
Das Ergebnis der Anfrage enthält daher neben der eigentlichen (Teil-)Ergebnismenge der Routing-Informationen (routes
) auch Informationen wie Anzahl (count
), Gesamtanzahl (totalCount
) und Startpunkt der Ergebnismenge (offset
).
Die zurückgegebene Teilergebnismenge ist standardmäßig auf 100 Einträge limitiert und kann über den GET-Parameter limit
auf maximal 500 Einträge erweitert werden.
Über den GET-Parameter offset
können weitere Teilmengen der Ergebnismenge ermittelt werden.
Der Endpunkt GET /routes
ist auf die Anzahl von Anfragen in Zeitfenstern beschränkt. Es kann also vorkommen, das der Dienst einen HTTP-Status-Code
429
zurückliefert. Um diese Beschränkung auswerten zu können liefert der Endpunkt entspechende RateLimit-Headers bei jeder Antwort zurück.
Zustellpunkte ermitteln
Beispiele für das Ermitteln der benötigten Daten:
- .NET (SDK)
- Direkt via URL
- curl
Das folgende Beispiel zeigt, wie Sie das .NET-SDK nutzen, um die zuständigen Zustellpunkte
für eine Verwaltungsleistung in einer bestimmten Region zu ermitteln.
Sie erzeugen zunächst eine Referenz auf den Routing-Client des SDKs.
Dafür benötigen Sie die Endpunkte der jeweiligen Betriebsumgebungen der FIT-Connect-Infrastruktur.
Eine Beschreibung der Umgebungen (Environments) finden Sie hier.
Das folgende Beispiel nutzt FitConnectEnvironment.Testing
. Der Parameter logger
ist optional.
var routingClient = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger);
Am Routing-Client rufen Sie dann die Methode FindDestinationsAsync(leikaKey, ars)
auf:
var routes = routingClient.FindDestinationsAsync(leikaKey, ars);
Für den Aufruf dieser Methode benötigen Sie den Leistungsschlüssel,
d. h., den Schlüssel für eine bestimmte Verwaltungsleistung,
zum Beispiel 99400048079000
.
Zudem benötigen Sie den Amtlichen Regionalschlüssel, den Amtlichen Gemeindeschlüssel oder die AreaId.
Eine Definition der Begriffe finden Sie hier.
Das Beispiel verwendet den Amtlichen Regionalschlüssel (ARS) und weist den Rückgabewert der Methode der Variablen routes
zu.
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Codebeispiele - examples der FITKO hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
Die Routing-API kann direkt manuell über den Aufruf der folgenden Links ausprobiert werden:
- via ARS: https://routing-api-testing.fit-connect.fitko.dev/v1/routes?ars=064350014014&leikaKey=99123456760610
- via AGS: https://routing-api-testing.fit-connect.fitko.dev/v1/routes?ags=06435014&leikaKey=99123456760610
- via Area-ID (siehe Abschnitt Verwaltungspolitische Gebiete ermitteln): https://routing-api-testing.fit-connect.fitko.dev/v1/routes?areaId=931&leikaKey=99123456760610
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99123456760610&ars=064350014014"
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99123456760610&ags=06435014"
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99123456760610&areaId=931"
Alternativ können Aufrufe auch über das "Try"-Feature in der API-Dokumentation des Endpunktes GET /routes
durchgeführt werden.
Ein Beispiel für eine Antwort des Routingdienstes findet sich in der in der API-Dokumentation der Routing-API im Endpunkt GET /routes
.
Sofern eine Destination-ID und die Adresse des zuständigen Zustelldienstes bereits bekannt sind, können die in einem Zustellpunkt hinterlegten technischen Parameter auch über den Endpunkt GET /v1/destinations/{destinationId}
der Submission API des zuständigen Zustelldienstes abgerufen werden (siehe unten).
Aufbau der Zustellpunkt-Informationen
Die Zustellpunkt-Informationen bestehen aus:
- Die Destination-ID (
destinationId
) des Zustellpunktes. - Die signierten Adressierungsinformationen (
destinationSignature
). - Das Zustellpunkt-Objekt (
destinationParameters
) mit folgenden Inhalten:- Die Adresse des zuständigen Zustelldienstes (
submissionUrl
). - Der Status (
status
) gibt an, ob der Zustellpunkt aktiv ist. Nur im Statusactive
können neue Einreichungen versendet werden. - Die Verwaltungsleistungen (
services
), die über diesen Zustellpunkt abgebildet werden, bestehend aus:- einem Identifikator der Verwaltungsleitung (
identifier
): Typischerweise entspricht dieser einem Leistungsschlüssel aus dem FIM-Baustein Leistungen (siehe Glossar). - einer Liste an zulässigen Fachdatenschemata (
submissionSchemas
): Hiermit legt das empfangende System fest, welchem Schema die übergebenen Fachdatensätze entsprechen müssen. Welches der angegebenen Schemata verwendet werden muss, bestimmt das sendende System aus dem eigenen fachlichen Kontext heraus. Wenn bspw. ein Antrag für einen Schwerbehindertenausweis gestellt wird, muss der Fachdatensatz aus den dort hinterlegten Schemata gemäß dem dortigen Schema für den Schwerbehindertenausweis (bspw. ein FIM/XFall Schema) entsprechen. - einer Liste an Regionen (
regions
), für die die Verwaltungsleistung angeboten wird.
- einem Identifikator der Verwaltungsleitung (
- Schlüssel-ID (Key-ID,
kid
) des öffentlichen Verschlüsselungsschlüssels (encryptionKid
): Empfangende Systeme veröffentlichen die Schlüssel-ID ihres Verschlüsselungsschlüssels für die Verschlüsselung von Einreichungen. Der dazugehörige JSON Web Key (JWK) ist in einer Antwort des Routingdienstes im AttributpublicKeys
enthalten und kann auch über den EndpunktGET /v1/destinations/{destinationId}/keys/{keyId}
abgefragt werden. - Die Liste der öffentlichen Schlüssel des Zustellpunktes (
publicKeys
) als JSON Web Key Set (JWKS). Siehe Artikel Verschlüsseln. - Die Liste der unterstüzten Metadaten-Schema (
metadataVersions
). Siehe Artikel zum Metadatensatz. - Die Liste der unterstüzten Rückkanäle (
replyChannels
).
- Die Adresse des zuständigen Zustelldienstes (
- Die Signatur des Zustellpunkt-Objekts (
destinationParametersSignature
). - Der Name der zuständigen Fachbehörde (
destinationName
). - Die URL des Logos der zuständigen Fachbehörde (
destinationLogo
).
Adressierungsinformationen (Parameter destinationSignature
)
Die im Parameter destinationSignature
hinterlegten Adressierungsinformationen sind vom Self-Service-Portal (SSP) signiert und wurden zuvor von der für den Zustellpunkt zuständigen Stelle im Portalverbund hinterlegt.
Payload der destinationSignature
Der dekodierte Inhalt (Payload) der Adressierungsinformationen sieht beispielhaft wie folgt aus (Leerzeichen und Zeilenumbrüche dienen ausschließlich der besseren Lesbarkeit):
{
"submissionHost": "submission-api-testing.fit-connect.fitko.dev",
"iss": "https://portal.auth-testing.fit-connect.fitko.dev",
"services": [
{
"gebietIDs": [
"urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:150850055055"
],
"leistungIDs": [
"urn:de:fim:leika:leistung:99108012005000"
]
}
],
"destinationId": "9162e3c9-5364-489a-9e99-aeb24eacc85c",
"iat": 1639572681,
"jti": "5b47d038-6e4d-4060-9da3-6720689287a1"
}
Signaturprüfung der Adressierungsinformationen (Parameter destinationSignature
)
Bei den vom Self-Service-Portal (SSP) signierten Adressierungsinformationen handelt es sich um einen signierten JSON Web Token (JWT). Durch eine Prüfung der Signatur des JWT kann eine Manipulation der Adressierungsinformationen durch die Systeme des Portalverbund sowie den Routingdienst ausgeschlossen werden.
Den zur Prüfung der Signatur benötigten öffentlichen Schlüssel (im Format JSON Web Key, kurz JWK) stellt das Self-Service-Portal stellt in einem JSON Web Key Set (JWKS) öffentlich zugänglich über den Endpunkt /.well-known/jwks.json
bereit.
Für unser Testsystem ist das JWKS z.B. hier verfügbar.
Ein Beispiel für ein JWKS ist in folgendem Ausschnitt dargestellt:
{
"keys": [
{
"alg": "PS512",
"e": "AQAB",
"key_ops": [
"verify"
],
"kid": "aeBUhQS8uaJvtzMcTyiEAN3KW4m65uDmL0X1AAIqdCE",
"kty": "RSA",
"n": "4Y0sJhadfrQnNZXeS7Pqh73FvtFPXLvLw11h7OiZM0DlqvRNgoYHO5k-kxJKOVCaFek0LjKM1_VQxMVpdChCkHeapdTg60oQTQZj3pG0boR3LStbqN3hNEx_JZC4aHH16kau0vqBBPiOOoq-ExUz-hXz_GMLsp9QVqIkw9okO_tzNPjQOo--GM8r4eSsKzgSHZzmepc9Gfk16eraGicBevlkclk32TmWIE_ErD31dtVbBlK-7GG2NUe-o_5rkiCJ2EwKRHZlLkBYJkkj_IjeUdKc4dawXoE8L83DSBPyapX47_L1VHTnT0hJdOVe6WHtvzzpusZ0Au-YDhp6LSwXnU9d0-VzBJmQvtrep1FM0d9aQrz0e0lVf8wCn13VdKO_FBZw9D7i0XRhF8JqQRblqhcCY7UGshbTTM8HORMFONHFmSQm10qfV29PLmztOhIuubMyYe1DPnlfRkpn5jnt8IPoopl6MliDKSc3m4dgG23KylBpTLr3U-XGQrTlerjrYh4t1LXiJ-jQhLefkak_WnExZJSXv601BgmbGj3GdIhS6lxdMX62cOuwKLVISOmHHxvimpQwhtYwiFR9OmGoKVgtCQ5eMKLwGWVwXSvUJ5YXH-yUyNW1_vOrt0DAtYmXwS_Ij0bMg9WoXKJ-5NtQpnnIzw1lr5bW5fNn2TgWpHk"
}
]
}
Zur Prüfung der Signatur der Adressierungsinformationen muss der passende Schlüssel mit der im kid
-Header der Adressierungsinformationen hinterlegten Schlüssel-ID im JWKS ermittelt werden.
Vor der eigentlichen Signaturprüfung muss die Einhaltung einiger Grundvoraussetzungen geprüft werden:
- Prüfung auf erlaubten Algorithmus
PS512
im Header des JWT gemäß den Vorgaben für kryptographische Verfahren. - Prüfung, dass der öffentliche Schlüssel eine Länge von 4096 bit besitzt.
- Prüfung, dass der öffentliche Schlüssel eine Verwendung des Algorithmus
PS512
erlaubt.
Anschließend kann mit dem ermittelten öffentlichen Schlüssel und einer entsprechenden Bibliothek eine Signaturprüfung erfolgen.
Prüfung von Leistungsschlüssel/ARS
Zusätzlich sollte geprüft werden, ob der gegenüber der Routing-API angefragte Leistungsschlüssel und ARS zu einer der in den Adressierungsinformationen angegeben Kombinationen aus Leistungsschlüssel und ARS passt. Erfolgt eine Anfrage auf Basis einer über die Routing API ermittelten Area-ID (anhand einer Postleitzahl bzw. eines Gebietsnamens), so muss an dieser Stelle auf das korrekte Mapping des Routingdienstes zw. Postleitzahl/Gebietsnamen und ARS vertraut werden, sodass eine Prüfung des ARS durch den Sender keinen Mehrwert bietet und daher ausgelassen werden kann.
Code-Beispiel
- .NET (SDK)
- Java (Spring)
Diese Funktionalität wird durch das SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode ClientFactory.GetRoutingClient(...).FindDestinationsAsync(...)
bereits automatisch mit abgedeckt:
var routes = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger)
.FindDestinationsAsync(leikaKey, ars);
Der Quellcode ist hier beschrieben.
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 int PUBLICKEYSIZE = 4096;
static final String SSP_BASE_URL = "https://portal.auth-testing.fit-connect.fitko.dev";
SignedJWT signedJWT = SignedJWT.parse(destinationSignature);
String requestedServiceIdentifier = "urn:de:fim:leika:leistung:100";
String requestedRegion = "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:11111";
Boolean validJWT = validateToken(signedJWT, requestedServiceIdentifier, requestedRegion);
static boolean validateToken(SignedJWT signedJWT, String requestedServiceIdentifier, String requestedRegion) {
try {
validateHeader(signedJWT.getHeader());
validatePayload(signedJWT.getJWTClaimsSet(), requestedServiceIdentifier, requestedRegion);
return verifySSPSignature(signedJWT);
} catch (ParseException e) {
throw new RuntimeException("The payload of the SET could not get parsed properly.");
}
}
static 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("jwt"), "The provided typ in the SET header is not jwt");
validateTrueOrElseThrow(header.getKeyID() != null, "The kid the SET was signed with is not set.");
}
static private void validatePayload(JWTClaimsSet payload, String requestedServiceIdentifier, String requestedRegion) throws ParseException {
validateTrueOrElseThrow(payload.getClaim("iss") != null, "The claim iss is missing in the payload of the JWT.");
validateTrueOrElseThrow(payload.getClaim("iat") != null, "The claim iat is missing in the payload of the JWT.");
validateTrueOrElseThrow(payload.getClaim("jti") != null, "The claim jti is missing in the payload of the JWT.");
validateTrueOrElseThrow(payload.getClaim("destinationId") != null, "The claim destinationId is missing in the payload of the JWT.");
validateTrueOrElseThrow(payload.getClaim("submissionHost") != null, "The claim submissionHost is missing in the payload of the JWT.");
// check if requested region/service matches an entry of the services list
validateTrueOrElseThrow(payload.getClaim("services") != null, "The claim services is missing in the payload of the JWT.");
validateTrueOrElseThrow(!((JSONArray) payload.getClaim("services")).isEmpty(), "At least one service is needed.");
validateTrueOrElseThrow(
((JSONArray) payload.getClaim("services")).stream().anyMatch(service -> (
((JSONArray) ((JSONObject) service).get("gebietIDs")).contains(requestedRegion) &&
((JSONArray) ((JSONObject) service).get("leistungIDs")).contains(requestedServiceIdentifier)
)
),
String.format("Requested region '%s' or requested serviceIdentifier '%s' not found", requestedRegion, requestedServiceIdentifier));
}
static private void validateTrueOrElseThrow(boolean expression, String msg) {
if (!expression) {
throw new RuntimeException(msg);
}
}
static boolean verifySSPSignature(SignedJWT signedJWT) throws IOException, ParseException, JOSEException {
// validate JWS algorithm
if ( !JWSAlgorithm.PS512.equals(signedJWT.getHeader().getAlgorithm()) )
throw new RuntimeException("JWSAlgorithm should be PS512!");
// retrieve JWK from self-service portal
JWKSet jwks = JWKSet.load(new URL(SSP_BASE_URL + "/.well-known/jwks.json"));
String keyId = signedJWT.getHeader().getKeyID();
JWK publicKey = jwks.getKeyByKeyId(keyId);
// validate JWK key size
if (publicKey.size() < PUBLICKEYSIZE) {
throw new RuntimeException("The key specified for signature verification is not of size 4096 bit.");
}
// validate JWK algorithm
if (publicKey.getAlgorithm() != JWSAlgorithm.PS512) {
throw new RuntimeException("The key specified for signature verification doesn't use/specify PS512 as algorithm.");
}
// verify signature
JWSVerifier jwsVerifier = new RSASSAVerifier(publicKey.toRSAKey());
return signedJWT.verify(jwsVerifier);
}
Signaturprüfung der vom DVDV gelieferten Zustellpunkt-Parameter (destinationParameters
bzw. HTTP-Header jws-signature
)
Die vom DVDV gelieferten Daten enthalten den Zustellpunkt sowie dessen Signatur (destinationParametersSignature
).
Die destinationParametersSignature
wird in der Routing API im gleichnamigen Feld und in der API des DVDV über den HTTP-Header jws-signature
zurückgegeben.
Bei der Signatur handelt es sich um eine JSON Web Signature (JWS) in der Compact Serialization gemäß RFC 7515, Abschnitt 3.1.
Die Signatur liegt als Detached Signature gemäß RFC 7515, Anlage F vor.
Beispiel eines Zustellpunktes (destinationParameters
):
{
"encryptionKid": "NFNb7k84r61G9ayAAJItJCNGl7wKWif9HyBAgicJq_8",
"metadataVersions": ['1.0.0'],
"publicKeys":
{
"keys":
[
{
"alg": "RSA-OAEP-256",
"e": "AQAB",
"key_ops": ["wrapKey"],
"kid": "NFNb7k84r61G9ayAAJItJCNGl7wKWif9HyBAgicJq_8",
"kty": "RSA",
"n": "1f1070XZ4NpHN2WqdH5c8dBUBPH99TJEvVXSP_jjZdOEzRJztUwSpIabtAvgDnNGmPTLs-jLlVR3NQCyKwOwpHVi3FmudKmIPplBFpsEpZ9JYBGpg8_ZbDN9fwJhob0KjAlsSY9mBOTfqLCqqVIJrk4fxBjwNaroCLkSbS2RrfMtUEW5T5Vo1uw2lnYTKq1uyhr1PG02mvDCBb0LMAqcMXRR6bdme8GN55S3UNWhsaonpq04aa8_baVdjoJYTk03VLORMojnnrJjxyPPiHRs2Re9JQoaVPy6TUrbFV63zvt30XM8ZJnla09yhMmuBJXpdtyWXKnKyqj8m9D5Vg68xksQVeJozpCAoBlsJeAheE31XPQwCBvamy46K669ZCkfkdhQgoIJMt1AVSef0qcLDg__nQ-rfIuYxHrtn7jgI0NeCGFbscxmzl08_LSj3nlj2-ag2uVq4bbdH3tziNxy_rr84N-6AA5iQe5v1L_zYXYWxGzaAOUWzJt0QRiEC9pF6Zqfrn4mPHn5lm2jtdM9AlmgkZtmK92rByfcMzo5-yEK37K96NtqpDCsoABUkvC1TLiqaCkGkQd1DmGnfNyGJV_eNMwmZyotom8WLS-icbQD913F9YlSTRsQYhFzw78pDJHHo4AtldMiQcpUY4qoVVpfpPZlMWTq7idnq6iO4MM",
"x5c": [
"...(base64 encoded cert)...",
"...(base64 encoded intermediate cert)...",
"...(base64 encoded root cert)..."
]
},
{
"alg": "PS512",
"e": "AQAB",
"key_ops": ["verify"],
"kid": "QEIaM4Lz9KLSaPyDzis-6aqE1x8q82iGdTv8Gb64ves",
"kty": "RSA",
"n": "lvi7t8Xy9Ef36gooR-AcsL4BBXjBKO0wcpM_hwFyQAELRrC3l5TityJPGjhFZJo1HZSFWGFCvqG56KkPgT5UdqbHFN2watpSguNTalERFmr6cXeB65NdjCrglCsGBmMHVViyCZ2gaBlfppdpeo0BI_9gUqv0OhzzoJGunI7G2YwPQrCdyEWWRYNVLqd_4B1wPPyc5MONavF1pQ6e1Nlk4_c9nL4A51-LRXlmBODhQ41aAac1gjpLD-ht1bVIjGzTUKZku5THAOltsEcDhjYXyfEUTA83i3I8PY9KMoenDFbCrtSUwsX-usJbpOt3M85TNAGxJeWIMa8nIEDyp1vjjb6QcPwvQgfFkSzjTQchVO4cT3rQ-DzqaRso9PAqOS0J3xasaKsqdgcr0AYvAdME3Yw_Wg2YLsq4GKjVtCAdhZiHBxx1H6oYFNXjYsnrMs99u_TpipFaDhJ-iWqtY7Bo_aek2yEt3mAoKByFO6QoQZoOgYteqCvJVGKOzETYgR9OA3_CSzl9YdkF2J3BH3DeBSX80_hSD-aDHGuRClzQ8iMBnAPW4HiOZ9IBkll-Ma7pIcgIXWhAnwvUCxtnK7ZPk213uhcwPxdK8wxxldzjsvVisFZw3QeJ7t-XMpm5_0gL5jGjzq0BAyHcxFr8yJxCekBqrBLoEHjV5LP6dUN-i98",
"x5c": ["MIIFCTCCAvECBARRtXowDQYJKoZIhvcNAQENBQAwSTELMAkGA1UEBhMCREUxFTATBgNVBAoMDFRlc3RiZWhvZXJkZTEjMCEGA1UEAwwaRklUIENvbm5lY3QgVGVzdHplcnRpZmlrYXQwHhcNMjEwOTE2MTYwMTU1WhcNMzEwOTE0MTYwMTU1WjBJMQswCQYDVQQGEwJERTEVMBMGA1UECgwMVGVzdGJlaG9lcmRlMSMwIQYDVQQDDBpGSVQgQ29ubmVjdCBUZXN0emVydGlmaWthdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJb4u7fF8vRH9+oKKEfgHLC+AQV4wSjtMHKTP4cBckABC0awt5eU4rciTxo4RWSaNR2UhVhhQr6hueipD4E+VHamxxTdsGraUoLjU2pRERZq+nF3geuTXYwq4JQrBgZjB1VYsgmdoGgZX6aXaXqNASP/YFKr9Doc86CRrpyOxtmMD0KwnchFlkWDVS6nf+AdcDz8nOTDjWrxdaUOntTZZOP3PZy+AOdfi0V5ZgTg4UONWgGnNYI6Sw/obdW1SIxs01CmZLuUxwDpbbBHA4Y2F8nxFEwPN4tyPD2PSjKHpwxWwq7UlMLF/rrCW6TrdzPOUzQBsSXliDGvJyBA8qdb442+kHD8L0IHxZEs400HIVTuHE960Pg86mkbKPTwKjktCd8WrGirKnYHK9AGLwHTBN2MP1oNmC7KuBio1bQgHYWYhwccdR+qGBTV42LJ6zLPfbv06YqRWg4SfolqrWOwaP2npNshLd5gKCgchTukKEGaDoGLXqgryVRijsxE2IEfTgN/wks5fWHZBdidwR9w3gUl/NP4Ug/mgxxrkQpc0PIjAZwD1uB4jmfSAZJZfjGu6SHICF1oQJ8L1AsbZyu2T5Ntd7oXMD8XSvMMcZXc47L1YrBWcN0Hie7flzKZuf9IC+Yxo86tAQMh3MRa/MicQnpAaqwS6BB41eSz+nVDfovfAgMBAAEwDQYJKoZIhvcNAQENBQADggIBAD4iEnx80ouIv6AhENlQM2vSy8h3SeuKxKrzOiliLLyYnXcaLSGjb4i5xheOnSiKeLvd/pm+dQlXzDYHYBNWXNnWXocXQQGRhx8Vvi3hiR1pEPKCvZcGJYoLQ/9Qvoa0JTw+ikThdg7V8Q0qhvYrM3utf5SG4+P3M3xa8rsEqFW9BQchxxmPnHHoBT2bek1UOjqZp9FLUKI1dOX8XUl8ptVlA+YUU3HESDNcY981fp+fWqcEGDIlawXw4oNwZ+tbGdIJecb4FLbxVdKOWh4klLxgu+SBOtb0wXmmVcoRoxwu2/MCYF3j73FyhVw9YJbRAZyj2VraYjZRcanRxfVBfFBexSiTE7rwU3Uh1fZBvsKzq0KoebC/wQ32ANWS7OaEh6ryDxb2+etymnOplyrTX8KTEPePgT+MLdmhFkOTyYxw5naisSXNIZctHEKshtQOutlGoJyXbDu2t08/O1HlP0qjvFIYeN94ohdG29RydkecBu8ixrAd6YUkYEMLgv71xzx40RVVg6IKg4Rekmjx126oDAYAtUFqHK1MD0pkUOTvhD2G0u4FKeYxd/Wh0tcb9hfl6fYRXptYRq3dxFKOyhki0jwVftFtmJHkBzts1M1agR6v036A7eFA/nT6HRwGJ7B3P2SOGBbZSNKk9JUnK7d3o5lhs98tZ8oCRTi1e4Ht"]
}
]
},
"submissionSchemas": [
{
"mimeType": "application/xml",
"schemaUri": "urn:xoev-de:bmk:standard:xbau_2.2#baugenehmigung.antrag.0200"
}
],
"submissionUrl": "https://submission-api.fit-connect.example.org/v1",
}
Bei einer Detached Signature sind die eigentlichen Inhaltsdaten (Payload) nicht Teil der übertragenen Signatur, sondern werden separat übermittelt. Für eine Signaturprüfung müssen die separat übermittelten Inhaltsdaten (Payload) daher zunächst wieder zur Signatur hinzugefügt werden (siehe unten).
Beispiel einer Detached JWS (destinationParametersSignature
):
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjNTNjd6UVE3QWFMdHFuSkthV2Y3N0FjM1d3WTItTEtXVVZTM1dJRUsyY2sifQ..At8ecdCMaUHQIo-22FKHBIocZ7hFxIHQxo0u_61tMKvDUy_BStKt46Aqh7lt41OLfIwEu6b4ZjLmWTeMi5SV-KMEAdRnYp5_WAmVhJttDkpf3d32uBcql0xJChAd93mr4qUnMvo-p3ltYgiKkG6_IHt8lUPt6BaH_BqWvidmtM5_Hd1SyW92OkEm1d50eMIJMxwY2e7_4qGKWqnMTel-dmY4HXlfqwfkkwkfvzVxQgOLtw0pzPKx3qODxuiOx6CVzr_QTrs12ugt7YkVkXSuVT40vekPw006O6aDOyETITK6OmDGnAB3iRqEX_qcOco2GWT0o66J2IARbtd12Hd0OpI0seqLONmSGNxcIxMcQwp5SAQa92xKkGFHgW86zAi6DQGggAh-Pb7MMN-i20jv1iC5hEpcd_eSKFDAGp_paEJkjOy-WPyhsksssdpival9OkXCQmHQnGzqu-RS4CS5l3gkAL-q5HoZHGD-YbXaYXRac4nyCrvmYg5yDBFSOvi4v07bokXiIp52tVAhuagmishsIDPI52ke4hkhHP68mUIQaA8UBhekNxPoEpXWfNwBmJpMc4Aa17Ko5WGYPG01_w11EXZ2q80-T4eINWdfAZAhrrmJwSmPQwnsrnuwTTOpl89vSeXi17KM-VxAf9kGrvE5scAtwGEjTlr56LEUieI
Erzeugung der vollständigen Signatur inklusive Payload
Die Prüfung der Signatur kann nur auf einer vollständigen Signatur erfolgen.
Für die Umwandlung einer Detached Signature in eine gewöhnliche JSON Web Signature muss der Payload (destinationParameters
) als Base64-URL-Encoded String der Signature hinzugefügt werden.
Dabei ist für das JSON des Payloads zu beachten, dass
- alle semantisch unbedeutenden nicht-druckbaren Zeichen (Leerzeichen, Tabs, Line Feed
\n
, Carriage Return\r
) vor und nach den strukturierenden Zeichen ([, {, ], }, :, ,) aus dem JSON-Payload entfernt werden und - die Attribute des JSON-Objekts in alphabetischer Reihenfolge sortiert werden. Die Sortierung ist unabhängig von der Groß-/Kleinschreibung der Attribute.
- .NET (SDK)
- Java
Diese Funktionalität wird durch das SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode ClientFactory.GetRoutingClient(...).FindDestinationsAsync(...)
bereits automatisch mit abgedeckt:
var routes = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger)
.FindDestinationsAsync(leikaKey, ars);
Der Quellcode ist hier beschrieben.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.SignedJWT;
// Object mapper that sorts properties alphabetically at serialisation
private static final ObjectMapper MAPPER = getConfiguredJsonMapper();
private SignedJWT combineDetachedSignatureWithPayload(final Route route) throws ParseException, JsonProcessingException {
final SignedJWT detachedSignature = SignedJWT.parse(route.getDestinationParametersSignature());
final Base64URL encodedDetachedPayloadPart = getBase64EncodedDetachedPayload(route);
final Base64URL headerPart = detachedSignature.getHeader().getParsedBase64URL();
final Base64URL signaturePart = detachedSignature.getSignature();
return new SignedJWT(headerPart, encodedDetachedPayloadPart, signaturePart);
}
private Base64URL getBase64EncodedDetachedPayload(final Route route) throws JsonProcessingException {
final RouteDestination detachedPayload = route.getDestinationParameters();
final String cleanedDetachedPayload = Strings.cleanNonPrintableChars(MAPPER.writeValueAsString(detachedPayload));
return Base64URL.encode(cleanedDetachedPayload.getBytes(StandardCharsets.UTF_8));
}
private static JsonMapper getConfiguredJsonMapper() {
return JsonMapper.builder()
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.configure(SerializationFeature.INDENT_OUTPUT, false)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}
Serialisierung
var parameterJson = JsonConvert.SerializeObject(route.DestinationParameters,
new JsonSerializerSettings {
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None,
ContractResolver = new OrderedContractResolver()
});
ContractResolver
public class OrderedContractResolver : DefaultContractResolver {
protected override System.Collections.Generic.IList<JsonProperty> CreateProperties(
System.Type type, MemberSerialization memberSerialization) {
NamingStrategy = new CamelCaseNamingStrategy();
return base.CreateProperties(type, memberSerialization).OrderBy(p => p.PropertyName)
.ToList();
}
}
Prüfung der vollständigen Signatur
Um die Signatur zu überprüfen, ist es notwendig auf die verwendeten Schlüssel (im Format JSON Web Key, kurz JWK) 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.
Da der Zustelldienst mit mehreren Instanzen betrieben werden kann, kann es auch mehrere Endpunkte zum JWKS geben.
Diese Endpunkte können dynamisch aus dem Payload (Zustellpunkt
) erzeugt werden.
Dabei setzt sich der Endpunkt aus dem Attribut submissionUrl
+ '/.well-known/jwks.json' zusammen.
Vor der eigentlichen Signaturprüfung muss die Einhaltung einiger Grundvoraussetzungen geprüft werden:
- Prüfung auf erlaubten Algorithmus
PS512
im Header des JWT gemäß den Vorgaben für kryptographische Verfahren. - Erweiterung der URL des zuständigen Zustelldienstes (
submissionUrl
) aus dem Payload des JWT (destinationParameters
) um den Pfad/.well-known/jwks.json
. Dabei muss geprüft werden, ob die URL zu einem vertrauenswürdigen Zustelldienst gehört. - Ermitteln des öffentlichen Schlüssel über die Key-ID (
kid
), die im Header des JSON Web Signature hinterlegt ist. - Prüfung, dass der öffentliche Schlüssel eine Länge von 4096 bit besitzt.
- Prüfung, dass der öffentliche Schlüssel eine Verwendung des Algorithmus
PS512
erlaubt.
Anschließend kann mit dem ermittelten öffentlichen Schlüssel und einer entsprechenden Bibliothek eine Signaturprüfung erfolgen.
- .NET (SDK)
- Java
Diese Funktionalität wird durch das SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode ClientFactory.GetRoutingClient(...).FindDestinationsAsync(...)
bereits automatisch mit abgedeckt:
var routes = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger)
.FindDestinationsAsync(leikaKey, ars);
Der Quellcode ist hier beschrieben.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
private final static int PUBLICKEYSIZE = 4096;
private final static String DVDV_SUBMISSIONURL_KEY = "submissionUrl";
private final static String KEYSTORE_URL_ENDING = ".well-known/jwks.json";
private final static String[] VALID_KEYSTORE_URLS = new String[]{ "https://submission-api-testing.fit-connect.fitko.dev/v1/.well-known/jwks.json"}; // this value depends on the environment (test, stage, prod) in which these checks are done. A list of valid submission API urls can be retrieved via https://portal.auth-{testing,refz,prod}.fit-connect.fitko.dev/v1/delivery-services
protected void validateBySignedJWT(SignedJWT signedJWT) throws BadJOSEException, JOSEException, IOException, ParseException
{
//1. Prüfung auf erlaubten Algorithmus PS512
if ( !JWSAlgorithm.PS512.equals(signedJWT.getHeader().getAlgorithm()) )
throw new RuntimeException("JWSAlgorithm should be PS512!");
//Key für den PublicKey aus dem Header des JWT ermitteln
String keyID = signedJWT.getHeader().getKeyID();
if ( keyID == null )
throw new RuntimeException("Header KeyId should not be null!");
//2. URL zum PublicKey Keystore ermitteln
Optional<URL> keyStoreUrl = getKeyStoreUrl(getBaseKeyStoreUrl(signedJWT));
if ( keyStoreUrl.isEmpty() )
throw new RuntimeException("No JWKSetUrl found in Payload!");
validateKeyStoreUrl(keyStoreUrl.get());
//KeyStore laden
JWKSet jwks = getJWKSet(keyStoreUrl.get());
//3. Public Key ermitteln
JWK publicKey = jwks.getKeyByKeyId(keyID);
if ( publicKey == null )
throw new RuntimeException("PublicKey should not be null!");
//4. Public Key muss eine Länge von 4096 bit haben!
if ( publicKey.size() < PUBLICKEYSIZE )
throw new RuntimeException("The key specified for signature verification is not of size 4096 bit.");
//5. Der PublicKey muss den Algorithmus PS512 verwenden!
if( !JWSAlgorithm.PS512.equals(publicKey.getAlgorithm()) )
throw new RuntimeException("The key specified for signature verification doesn't use/specify PS512 as algorithm.");
//Eigentliche Prüfung der Signatur
if (!signedJWT.verify(new RSASSAVerifier(publicKey.toRSAKey())))
throw new RuntimeException("Signature of payload not valid!");
}
protected String getBaseKeyStoreUrl(SignedJWT signedJWT)
{
Map<String, Object> payload = signedJWT.getPayload().toJSONObject();
return String.valueOf(payload.get(DVDV_SUBMISSIONURL_KEY));
}
protected Optional<URL> getKeyStoreUrl(String baseKeyStoreUrl)
{
if (baseKeyStoreUrl==null)
throw new RuntimeException("KeyStoreUrl not set!");
if (!baseKeyStoreUrl.endsWith("/"))
baseKeyStoreUrl+="/";
baseKeyStoreUrl+= KEYSTORE_URL_ENDING;
try
{
return Optional.of(new URL(baseKeyStoreUrl));
}
catch (MalformedURLException e)
{
throw new RuntimeException("KeyStoreUrl not valid! MalformedURLException for url " + baseKeyStoreUrl);
}
}
public void validateKeyStoreUrl(URL url)
{
if ( !VALID_KEYSTORE_URLS.contains(String.valueOf(url)) )
throw new RuntimeException("KeyStoreUrl not valid!");
}
Verwaltungspolitische Gebiete ermitteln
Falls für die Abfrage der destinationId
kein amtlicher Gemeindeschlüssel oder ein amtlicher Regionalschlüssel bekannt ist, kann über den Endpunkt GET /areas
nach passenden verwaltungspolitischen Gebieten gesucht werden.
Der Endpunkt GET /areas
implementiert Pagination.
Das Ergebnis der Anfrage enthält daher neben der eigentlichen (Teil-)Ergebnismenge der Gebiet-Informationen (areas
) auch Informationen wie Anzahl (count
), Gesamtanzahl (totalCount
) und Startpunkt der Ergebnismenge (offset
).
Die zurückgegebene Teilergebnismenge ist standardmäßig auf 100 Einträge limitiert und kann über den GET-Parameter limit
auf maximal 500 Einträge erweitert werden.
Über den GET-Paramter offset
können weitere Teilmengen der Ergebnismenge ermittelt werden.
Die id
eines Gebietes aus der Ergebnismenge kann im Endpunkt GET /areas
als Identifikator (areaId
) eines verwaltungspolitischen Gebietes verwendet werden.
Der Endpunkt GET /areas
ist auf die Anzahl von Anfragen in Zeitfenstern beschränkt. Es kann also vorkommen, das der Dienst einen HTTP-Status-Code
429
zurückliefert. Um diese Beschränkung auswerten zu können liefert der Endpunkt entspechende RateLimit-Headers bei jeder Antwort zurück.
Beispiele für die Suche:
- .NET (SDK)
- curl
Suche mit Postleitzahlanfang:
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/areas?areaSearchexpression=061*"
Suche mit Name:
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/areas?areaSearchexpression=Halle"
Suche mit Name und Postleitzahl:
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/areas?areaSearchexpression=06108&areaSearchexpression=Halle"
Beispiel für die Response
{
"count": 3,
"offset": 0,
"totalCount": 3,
"areas": [
{
"id": "16688",
"name": "Halle (Saale)",
"type": "kreisfreie Stadt"
},
{
"id": "16707",
"name": "Halle (Saale) - OT Altstadt",
"type": "Gemeindeteil"
},
{
"id": "16725",
"name": "Halle (Saale) - OT Nördliche Innenstadt",
"type": "Gemeindeteil"
}
]
}
Das folgende Beispiel zeigt, wie Sie die Methode GetAreas
der Klasse Router
des .NET-SDKs verwenden, um nach der AreaId anhand der Postleitzahl zu suchen.
Falls nur der Anfang der Postleitzahl bekannt ist, können Sie das Zeichen * für die fehlenden Stellen setzen. Die Methode gibt alle AreaIds zurück mit diesem Anfang der Postleitzahl:
var router = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger);
string filter = "061*";
var areaIds = router.GetAreas(filter).Result;
Sie können der Methode GetAreas
auch einen Namen übergeben, um die AreaIds aller Gebiete mit diesem Namen zu erhalten:
var router = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger);
string filter = "Halle";
var areaIds = router.GetAreas(filter).Result;
Sie können zudem der Methode GetAreas
eine Postleitzahl (im folgenden Beispiel "06108") und einen Gebietsnamen übergeben, indem Sie dem Namen "&areaSearchexpression=" voranstellen:
var router = ClientFactory.GetRoutingClient(FitConnectEnvironment.Testing, logger);
string filter = "06108&areaSearchexpression=Halle";
var areaIds = router.GetAreas(filter).Result;
Zustellpunkt-Informationen über die Submission API ermitteln
Zum Abruf der Zustellpunkt-Informationen stellt die Submission API einen Endpunkt bereit, der über Angabe des Parameters destinationId
die technischen Parameter der Einreichung für den jeweiligen Zustellpunkt ausgibt. Diese kann genutzt werden, wenn die destinationId
bereits bekannt ist. Die angebotenen Informationen über eine Destination unterscheiden sich fachlich nicht von den Information aus der Routing API. Bei der Submission API muss lediglich der Verschlüsselungsschlüssel über einen zusätzlichen Endpunkt abgerufen werden, anstatt diesen zusammen mit den anderen Informationen in einer Response zu erhalten (siehe Artikel Verschlüsseln).
Die URL der Submission API findet sich im Artikel Betriebsumgebungen.
- curl
- .NET (SDK)
Über curl
können diese Information mit dem folgenden Aufruf abgerufen werden.
$ export SUBMISSION_API=https://submission-api-testing.fit-connect.fitko.dev
$ export JWT_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJJc3N1Z...NL-MKFrDGvn9TvkA
$ export DESTINATION_ID=9162e3c9-5364-489a-9e99-aeb24eacc85c
$ curl \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-X GET "$SUBMISSION_API/v1/destinations/$DESTINATION_ID"
{
"destinationId": "9162e3c9-5364-489a-9e99-aeb24eacc85c",
"status": "created",
"services": [
{
"identifier": "urn:de:fim:leika:leistung:99108012005000",
"submissionSchemas": [
{
"schemaUri": "https://schema.fitko.de/fim/s17000098_1.0.schema.json",
"mimeType": "application/json"
}
],
"regions": [
"DE150850055055"
]
}
],
"encryptionKid": "rZ2DkPobwJRGSIC23MZtrWdlqbkzWovmvhPHNOwqxMA",
"metadataVersions": [
"1.0.0"
],
"replyChannels": {
"eMail": {
"usePgp": true
}
}
}
Diese Funktionalität wird durch das SDK bereits intern umgesetzt
und ist zum Beispiel durch einen Aufruf der SDK-Methode ClientFactory.GetRoutingClient(...).FindAreasAsync(...)
und der SDK-Methode ClientFactory.GetRoutingClient(...).FindDestinationsAsync(...)
bereits automatisch mit abgedeckt.