diff --git a/docs/changelog.md b/docs/changelog.md index d8527e56dd491f5db95549c1e62f0d9577e2ccfb..bf0656e4717f7a5b305d34b0d5edd750f37fee30 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ Das Format dieser Datei basiert auf [Keep a Changelog](https://keepachangelog.com/de). +## 2022-02-07 +### Dokumentation +- Hinweise zur [Signaturprüfung der vom DVDV gelieferten Zustellpunkt-Parameter](./responsibilities/get-destination.mdx#checkDestinationParametersSignature) ergänzt + ## 2022-02-03 ### Zustelldienst 1.2.0 - Story: Der Zustelldienst schreibt nun zusätzliche Infos in die Events `submit-submission` und `notify-submission`. diff --git a/docs/responsibilities/get-destination.mdx b/docs/responsibilities/get-destination.mdx index 911effc1297d6783a3094d04ac047509d413efff..3b295461ef4eef29dc006d9b7ea2a6320fc66aa0 100644 --- a/docs/responsibilities/get-destination.mdx +++ b/docs/responsibilities/get-destination.mdx @@ -161,7 +161,7 @@ Der dekodierte Inhalt (Payload) der Adressierungsinformationen sieht beispielhaf } ``` -### Signaturprüfung der vom Self-Service Portal (SSP) signierten Adressierungsinformationen (Parameter `destinationSignature`) +### 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. @@ -273,6 +273,200 @@ Im folgenden Beispiel wird die Bibliothek [nimbus-jose-jwt](https://connect2id.c </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.