Skip to content
Snippets Groups Projects
Commit f339d43e authored by Michael Haidner's avatar Michael Haidner
Browse files

Die Seite 'Zustellpunkt ermitteln' in 'Versand von Einreichungen' verschoben

parent b645d7e2
No related branches found
No related tags found
1 merge request!219neu_zustellpunkt_ermitteln_dokumentation_neuer_einstieg (planning#494)
---
title: Zustellpunkt ermitteln
---
import ApiLink from '@site/src/components/ApiLink'
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## 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 `destinationId` der zuständigen Stelle und die Adresse des zuständigen Zustelldienstes ermittelt werden.
:::tip Hinweis
Die über das Self-Service-Portal erstellten Zustellpunkte sind in der Testumgebung nicht automatisch über die Routing-API auffindbar.
Dieses Feature ist als zukünftige Erweiterung geplant.
Für eine Auffindbarkeit der Zustellpunkte über die Routing-API ist in der Testumgebung derzeit eine manuelle Pflege in der Demo-Umgebung des Portalverbund Onlinegateway (PVOG) notwendig.
Sofern eine Destination-ID bereits bekannt ist, können die in einem Zustellpunkt hinterlegten technischen Parameter alternativ auch über den Endpunkt <ApiLink api="submission-api" to="/v1/destinations/{destinationId}" /> der Submission API des zuständigen Zustelldienstes [abgerufen werden (siehe unten)](#submissionapi).
Die Konfiguration des Antragsroutings in der produktiven Umgebung ist bereits jetzt möglich. Die [in den Landesredaktionen konfigurierten Zustellpunkte](../organisation-tasks/publish_destination.mdx) sind in der Produktivumgebung über Routing API abrufbar.
:::
Die Ermittlung der `destinationId` und die Ermittlung der technischen Parameter über die Routing-API erfolgt über einen GET-Request auf den Endpunkt <ApiLink api="routing-api" to="/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](https://fimportal.de/kataloge#download-leistungen)) verwendet werden.
- Einen Identifikator eines verwaltungspolitischen Gebietes. Für den Identifikator des verwaltungspolitischen Gebietes kann entweder der [amtliche Gemeindeschlüssel (AGS)](https://www.destatis.de/DE/Themen/Laender-Regionen/Regionales/Gemeindeverzeichnis/Glossar/amtlicher-gemeindeschluessel.html), der [amtliche Regionalschlüssel (ARS)](https://www.destatis.de/DE/Themen/Laender-Regionen/Regionales/_FAQ/regionalschluessel.html) oder die [Id eines Gebietes](#verwaltungspolitische-gebiete-ermitteln) aus der Suche über den Endpunkt <ApiLink api="routing-api" to="/areas" /> verwendet werden.
Der Endpunkt <ApiLink api="routing-api" to="/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-Paramter `offset` können weitere Teilmengen der Ergebnismenge ermittelt werden.
Der Endpunkt <ApiLink api="routing-api" to="/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](../getting-started/rate-limiting.md) bei jeder Antwork zurück.
Beispiele für das Ermitteln der benötigten Daten:
<Tabs
defaultValue="curl"
values={[
{ label: 'curl', value: 'curl', },
]
}>
<TabItem value="curl">
```bash
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99108012005000&ags=15085055"
```
```bash
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99108012005000&ars=150850055055"
```
```bash
$ export ROUTING_API=https://routing-api-testing.fit-connect.fitko.dev
$ curl \
-H "Content-Type: application/json" \
-X GET "$ROUTING_API/v1/routes?leikaKey=99108012005000&areaId=15529"
```
</TabItem>
</Tabs>
Beispiel für die Response:
```json
{
"count": 1,
"offset": 0,
"totalCount": 1,
"routes": [
{
"destinationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"destinationSignature": "eyJraWQiOiJmT0hzdExPNGJlQnEwcHgtMDFwTEoyUnhQbUJEakNtbEtIQk84ZzVXLVNBIiwidHlwIjoiand0IiwiYWxnIjoiUFM1MTIifQ.eyJzdWJtaXNzaW9uSG9zdCI6InN1Ym1pc3Npb24tYXBpLWRldi5maXQtY29ubmVjdC5maXRrby5kZXYiLCJpc3MiOiJodHRwczpcL1wvcG9ydGFsLmF1dGgtZGV2LmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInNlcnZpY2VzIjpbeyJnZWJpZXRJRHMiOlsidXJuOmRlOmJ1bmQ6ZGVzdGF0aXM6YmV2b2Vsa2VydW5nc3N0YXRpc3RpazpzY2hsdWVzc2VsOnJzOjEyMDY0NTQxMCJdLCJsZWlzdHVuZ0lEcyI6WyJ1cm46ZGU6ZmltOmxlaWthOmxlaXN0dW5nOjk5MTM0MDA1MDE3MDAwIl19XSwiZGVzdGluYXRpb25JZCI6ImFjNzE1ZjM0LTMzM2UtNDFmNC05YmI1LTE4NmIzMDllYTIzMyIsImlhdCI6MTYzNzg2MzQ4MSwianRpIjoiMmQxNDI2ZjItZDY3My00NmRlLTg2OGUtZDk2ODQ0ZDI0ZmUxIn0.gSfjbRck_BmhkVx-P-E9UexlQudEZV8auYTHXrCSM4ja3gDg2VGlpCjH3-WBvgdLp7zv0J0z9en6PecF73QV4ltik0c7j4tbpAPz9tmTu0pedjVrbkbWj4b4H-EyYt1IJeDyrZJglZ1EB4b_4mk5HNZHgZnbMx0QLhRci8-wJf76hJgoWHkebpXNjdHHqndFbpGa7HCiul1XeJVv8Ny6Fgb7Nu-c5-YmVl5kZSCmxURAZlZubk3jBaIfMOEXIth3B4FtOvEiEXkWTtH0r99eZkYdK-ykLuefenS_Ib56ZpZ67Sw3T-LuV5pIzhq--REL6PaCOvRkU88SS1iW8LmiwEIxCIwFNEpnohNYjy4ZG8CnCfD4SztRA9nQYohdh2Cc_3MafUX7wjz1vqlonmZ7m4QYfZCqtl3IkcJLeayBU5OHTlcvHAQRIfgvP9SJApJr_Y2p3p4fHePOVStxLMlCOCYcmf0EBibvUsuwbEbmeppP72OFOkCwA9I82Z0SnxLdaHLXup2f_z0OnJtxrJAZnhREyYSvL2HOJusNKfpNy360C7Kf2g-BzAEvD4K5LzqWhKrWgztn4SDmgWL_Z3Ez1e2ZqTzfmJXxE_WGit2Lr0rBd9vTPGYKidBLZ8B-2JJZCPbrqlTxPdWImPrgcZP0qpNsJdls4OJo7xz5ozwrR4Y",
"destinationParameters": {
"submissionUrl": "https://submission-api.fit-connect.example.org/v1",
"status": "active",
"submissionSchemas": [
{
"schemaUri": "urn:xoev-de:bmk:standard:xbau_2.2#baugenehmigung.antrag.0200",
"mimeType": "application/xml"
}
],
"encryptionKid": "NFNb7k84r61G9ayAAJItJCNGl7wKWif9HyBAgicJq_8",
"publicKeys": {
"keys": [
{
"kty": "RSA",
"key_ops": [
"wrapKey"
],
"alg": "RSA-OAEP-256",
"x5c": [
"...(base64 encoded cert)...",
"...(base64 encoded intermediate cert)...",
"...(base64 encoded root cert)..."
],
"kid": "NFNb7k84r61G9ayAAJItJCNGl7wKWif9HyBAgicJq_8",
"n": "1f1070XZ4NpHN2WqdH5c8dBUBPH99TJEvVXSP_jjZdOEzRJztUwSpIabtAvgDnNGmPTLs-jLlVR3NQCyKwOwpHVi3FmudKmIPplBFpsEpZ9JYBGpg8_ZbDN9fwJhob0KjAlsSY9mBOTfqLCqqVIJrk4fxBjwNaroCLkSbS2RrfMtUEW5T5Vo1uw2lnYTKq1uyhr1PG02mvDCBb0LMAqcMXRR6bdme8GN55S3UNWhsaonpq04aa8_baVdjoJYTk03VLORMojnnrJjxyPPiHRs2Re9JQoaVPy6TUrbFV63zvt30XM8ZJnla09yhMmuBJXpdtyWXKnKyqj8m9D5Vg68xksQVeJozpCAoBlsJeAheE31XPQwCBvamy46K669ZCkfkdhQgoIJMt1AVSef0qcLDg__nQ-rfIuYxHrtn7jgI0NeCGFbscxmzl08_LSj3nlj2-ag2uVq4bbdH3tziNxy_rr84N-6AA5iQe5v1L_zYXYWxGzaAOUWzJt0QRiEC9pF6Zqfrn4mPHn5lm2jtdM9AlmgkZtmK92rByfcMzo5-yEK37K96NtqpDCsoABUkvC1TLiqaCkGkQd1DmGnfNyGJV_eNMwmZyotom8WLS-icbQD913F9YlSTRsQYhFzw78pDJHHo4AtldMiQcpUY4qoVVpfpPZlMWTq7idnq6iO4MM",
"e": "AQAB"
}
]
},
"metadataVersions": [
"string"
],
"replyChannels": {
"eMail": {
"usePgp": true
},
"deMail": {},
"fink": {},
"elster": {}
}
},
"destinationParametersSignature": "eyJ0eXAiOiJKT1NFIiwiYWxnIjoiUFM1MTIiLCJraWQiOiJlOWJjMDk3YS1jZTUxLTQwMzYtOTU2Mi1kMmFkZTg4MmRiMGQiLCJjdHkiOiJhcHBsaWNhdGlvbi9qb3NlIn0..g9xgjWf-_3JjMAFwPgBB4iXFrkrsRS-Ois3pxWWcFzdZuu_I8jH9Bd4FAQpf6nJwPtytJgoWYkm7gTCMwKiQH7JknXXrdYcnHRrlU2aT9thjaK5uYKYvuDfvklvQvKTYtfqowkMtk3pl91TfB1Pyxbprx6u5qut_pI-z2E7SC8gJ6V8u1rT1wDHOp-xrvMHUQiH7Ugmyb7Tg_Dc55AL0FrZ2wmurdPK46iAZBfIpzNJgUbqrlKvKQkwbs11Bc2qRzrFIG8yMyuN-qhGxibokMoq1U3FjlxtNgwWQJJOYlMiCbMibkINsmZ5mGZDS_Dra89TVMz0_rZagj-mxJ5-DlIE7E1LsvUKXYBHhPFoCJTGH1Lla7AEWtZ59HNSalMjvgcGWTgp-xTbqo8Ej6PwUM9j7_lNH1kT0iSvuCTYktVJEmovmpT0gB9c0AaGbQfiZFk8UUiBSAVFoD2B-0EP8CkEDAxsL0xlDdSAeK4Zrvg2nnck8NjdDQa68KfW4Fp32cfdkWYhGJ13xNG889P0aofCv7Joj4zxVEsPGfvqe6b78i8oZOe_Tn2lIVBwxZ6phtvEmZ8w_aS4zGSuYXoXx1DOjwatvCuH3rKicbQpJwEX3Bbcmv-NSgVZDae4dntgc89zbPbAg1zsqZDyHublVSK8m7i5CVLKiDvXdoWidNbY",
"destinationName": "Einwohnermeldeamt",
"destinationLogo": "https://einwohnermeldeamt.beispielstadt.example.org/logo.png"
}
]
}
```
:::tip Hinweis
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 <ApiLink api="submission-api" to="/v1/destinations/{destinationId}" /> der Submission API des zuständigen Zustelldienstes [abgerufen werden (siehe unten)](#submissionapi).
:::
### Aufbau der Zustellpunkt-Informationen {#destination-object}
Die Zustellpunkt-Informationen bestehen aus:
- Die Destiantion-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 Status `active` 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](../glossary.md)).
- 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.
- 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 Attribut `publicKeys` enthalten und kann auch über den Endpunkt <ApiLink api="submission-api" to="/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](../sending/encrypt.mdx).
- Die Liste der unterstüzten Metadaten-Schema (`metadataVersions`). Siehe Artikel zum [Metadatensatz](../metadata/overview.mdx).
- Die Liste der unterstüzten Rückkanäle (`replyChannels`).
- Die Signatur des Zustellpunkt-Objekts (`destinationParametersSignature`).
- Der Name der zuständigen Fachbehörde (`destinationName`).
- Die URL des Logos der zuständigen Fachbehörde (`destinationLogo`).
### Inhalt der Adressierungsinformationen (Parameter `destinationSignature`)
Der dekodierte Inhalt (Payload) der Adressierungsinformationen sieht beispielhaft wie folgt aus (Leerzeichen und Zeilenumbrüche dienen ausschließlich der besseren Lesbarkeit):
```json
{
"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 vom Self-Service Portal (SSP) signierten Adressierungsinformationen (Parameter `destinationSignature`) {#checkDestinationSignature}
Bei den signierten Adressierungsinformationen handelt es sich um signierte JSON Web Tokens (JWTs).
Um die Signatur der vom Self-Service-Portal signierten JWTs zu überprüfen, ist es notwendig, auf die verwendeten Schlüssel (im Format JSON Web Key, kurz JWK) zugreifen zu können.
Das Self-Service-Portal stellt ein 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](https://portal.auth-testing.fit-connect.fitko.dev/.well-known/jwks.json) verfügbar.
Ein Beispiel für ein JWKS ist in folgendem Ausschnitt dargestellt:
```json
{
"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"
}
]
}
```
Mit diesem JWKS kann die Signatur der Adressierungsinformationen überprüft werden.
Hierfür muss der Schlüssel mit der passenden Schlüssel-ID (Key-ID) aus dem `kid`-Header der Adressierungsinformationen im JWKS ermittelt werden.
Dann kann man mit diesem und einer entsprechenden Bibliothek eine Signaturprüfung durchführen.
<Tabs
defaultValue="java"
values={[
{ label: 'Java (Spring)', value: 'java', },
]
}>
<TabItem value="java">
Im folgenden Beispiel wird die Bibliothek [nimbus-jose-jwt](https://connect2id.com/products/nimbus-jose-jwt) für die Prüfung genutzt.
```java
SignedJWT signedJWT = SignedJWT.parse(destinationSignature);
String keyId = signedJWT.getHeader().getKeyID();
String requestedServiceIdentifier = "urn:de:fim:leika:leistung:100";
String requestedRegion = "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:11111";
Boolean validToken = validateToken(signedJWT, requestedServiceIdentifier, requestedRegion);
Boolean validTokenSignature = verifySSPSignature(signedJWT, keyId);
Boolean validJWT = validToken && validTokenSignature;
```
```java
static boolean validateToken(SignedJWT signedJWT, String requestedServiceIdentifier, String requestedRegion) {
try {
validateHeader(signedJWT.getHeader());
validatePayload(signedJWT.getJWTClaimsSet(), requestedServiceIdentifier, requestedRegion);
return true;
} 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.");
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);
}
}
```
```java
static final String SSP_BASE_URL = "https://portal.auth-testing.fit-connect.fitko.dev";
static boolean verifySSPSignature(SignedJWT signedJWT, String keyId) throws IOException, ParseException, JOSEException {
JWKSet jwks = JWKSet.load(new URL(SSP_BASE_URL + "/.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 signedJWT.verify(jwsVerifier);
}
```
</TabItem>
</Tabs>
### Signaturprüfung der vom DVDV gelieferten Zustellpunkt-Parameter (`destinationParameters` bzw. HTTP-Header `jws-signature`) {#checkDestinationParametersSignature}
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](https://datatracker.ietf.org/doc/html/rfc7515#section-3.1).
Die Signatur liegt als Detached Signature gemäß [RFC 7515, Anlage F](https://datatracker.ietf.org/doc/html/rfc7515#appendix-F) vor.
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 Signaur hinzugefügt werden (siehe unten).
Beispiel eines Zustellpunktes (`destinationParameters`):
```json
{
"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",
}
```
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ändige 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 Payload 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.
<Tabs
defaultValue="java"
values={[
{ label: 'Java', value: 'java', },
]
}>
<TabItem value="java">
```java
int firstPoint = detachedSignature.indexOf(".");
Base64URL payload = Base64URL.encode(sortedAndCleanedPayloadJson):
SignedJWT signedJWT = new SignedJWT(
new Base64URL(headerParameterOfJWS.substring(0, firstPoint)),
payload,
new Base64URL(headerParameterOfJWS.substring(firstPoint + 2))
);
```
</TabItem>
</Tabs>
#### 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 <ApiLink api="submission-api" to="/.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:
1. Prüfung auf erlaubten Algorithmus `PS512` im Header des JWT gemäß den [Vorgaben für kryptographische Verfahren](../details/crypto.md).
2. 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.
3. Ermitteln des öffentlichen Schlüssel über die Key-ID (`kid`), die im Header des JSON Web Signature hinterlegt ist.
4. Prüfung, dass der öffentliche Schlüssel eine Länge von 4096 bit besitzt.
5. Prüfung, dass der öffentliche Schlüssel eine Verwendung des Algorithmus `PS512` erlaubt.
6. Prüfung der Signatur.
<Tabs
defaultValue="java"
values={[
{ label: 'Java', value: 'java', },
]
}>
<TabItem value="java">
```java
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"};
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 ( PUBLICKEYSIZE != publicKey.size() )
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.");
//6. Eigentliche Prüfung der Signatur
if (!signedJWT.verify(new RSASSAVerifier(publicKey.toRSAKey())))
throw new RuntimeException("Signature of payload not valid!");
}
```
```java
protected String getBaseKeyStoreUrl(SignedJWT signedJWT)
{
Map<String, Object> payload = signedJWT.getPayload().toJSONObject();
return String.valueOf(payload.get(DVDV_SUBMISSIONURL_KEY));
}
```
```java
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);
}
}
```
```java
public void validateKeyStoreUrl(URL url)
{
if ( !VALID_KEYSTORE_URLS.contains(String.valueOf(url)) )
throw new RuntimeException("KeyStoreUrl not valid!");
}
```
</TabItem>
</Tabs>
### 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 <ApiLink api="routing-api" to="/areas" /> nach passenden verwaltungspolitischen Gebieten gesucht werden.
Der Endpunkt <ApiLink api="routing-api" to="/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 <ApiLink api="routing-api" to="/areas" /> als Identifikator (`areaId`) eines verwaltungspolitischen Gebietes verwendet werden.
Der Endpunkt <ApiLink api="routing-api" to="/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](../getting-started/rate-limiting.md) bei jeder Antwork zurück.
Beispiele für die Suche:
<Tabs
defaultValue="curl"
values={[
{ label: 'curl', value: 'curl', },
]
}>
<TabItem value="curl">
Suche mit Postleitzahlanfang
```bash
$ 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
```bash
$ 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
```bash
$ 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"
```
</TabItem>
</Tabs>
Beispiel für die Response
```json
{
"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"
}
]
}
```
## Zustellpunkt-Informationen über die Submission API ermitteln {#submissionapi}
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](../sending/encrypt.mdx)).
:::note Hinweis
Die URL der Submission API findet sich im Artikel [Erste Schritte](../getting-started/get-started.mdx#testing).
:::
<Tabs
defaultValue="curl"
values={[
{ label: 'curl', value: 'curl', },
]
}>
<TabItem value="curl">
Über `curl` können diese Information mit dem folgenden Aufruf abgerufen werden.
```bash
$ 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
}
}
}
```
</TabItem>
</Tabs>
\ No newline at end of file
......@@ -45,9 +45,10 @@ module.exports = {
items: [
'sending/overview',
'sending/start-submission',
'sending/get-destination',
'sending/metadata',
'sending/encrypt',
'sending/attachments',
'sending/attachments',
'sending/submit',
'sending/query-status',
'sending/accept-reject',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment