Zum Hauptinhalt springen

Antworten senden und lesen (BiDiKo)

TL;DR – Onlinedienst erzeugt einmal pro Case ein Schlüsselpaar und legt den öffentlichen Schlüssel als ReplyChannel an die Submission. Verwaltungssystem antwortet verschlüsselt darauf; Onlinedienst holt die Antwort ab und entschlüsselt sie mit dem privaten Schlüssel.


Wer macht was?​

Typ C · Onlinedienst

Onlinedienst

  1. Schlüsselpaar erzeugen
  2. Submission mit ReplyChannel senden
  3. Weitere Submissions dem Case zuordnen
  4. Replies abholen & entschlüsseln
  5. Reply akzeptieren oder zurückweisen
Typ A/B · Verwaltung

Verwaltungssystem

  1. Submission empfangen
  2. caseId und Verschlüsselungsschlüssel persistieren
  3. Reply verschlüsselt senden

Ablauf im Überblick​

Alle Submissions und Replies eines Dialogs werden durch dieselbe caseId verklammert.

Eine Antwort adressiert keinen Zustellpunkt, sondern den Vorgang (Regel 3 des Rollenmodells). Und der Rückkanalschlüssel gehört zu einem Vorgang, nicht zum Onlinedienst — deshalb muss er pro Vorgang erzeugt und aufbewahrt werden, und deshalb nimmt das SDK beim Lesen keinen Schlüssel, sondern eine Nachschlagefunktion.


Onlinedienst — was Sie implementieren​

Typ C · Onlinedienst

Dieser Abschnitt richtet sich an Entwickler eines Onlinedienstes (Typ C).

Schlüsselpaar für den Rückkanal erzeugen​

Pro Vorgang (Case) erzeugen Sie genau ein Schlüsselpaar. Der private Schlüssel verbleibt sicher beim Onlinedienst und entschlüsselt später die Replies; der öffentliche Schlüssel wird der Initial-Submission als Teil des ReplyChannels mitgegeben.

JWKPair pair = EncryptionKeyPairGenerator.generate();

JWK privateReplyDecryptionKey = pair.privateKey(); // sicher verwahren!
JWK publicReplyEncryptionKey = pair.publicKey(); // im ReplyChannel mitsenden
Ephemeral Keys

Die generierten Schlüssel basieren nicht auf einem V-PKI-Zertifikat. Sie sind sogenannte Ephemeral Keys und dürfen nur für das Ver-/Entschlüsseln von Replies innerhalb eines Vorgangs eingesetzt werden – nicht beim Einrichten eines Zustellpunkts.

ReplyChannel an die Submission anhängen​

Der öffentliche Schlüssel wandert als ReplyChannel in die Submission — er sagt dem Verwaltungssystem, wie und womit es antworten soll:

var replyChannel = ReplyChannels.fitConnect(
publicReplyEncryptionKey,
List.of("urn:xoev-de:bmk:standard:xbau_2.3")
);

Empfänger adressieren, wie beim normalen Senden:

OnlineService onlineService = sdk.onlineService(myDestinationId);

Participant recipient = Participant.of(
destinationId,
Addressing.toService("urn:de:fim:leika:leistung:99400048079000", "FIT-Connect Demo"));

Und die Submission mit dem ReplyChannel senden:

OutgoingSubmission submission = OutgoingSubmission.to(recipient)
.setData(SubmissionData.json("{\"message\":\"Hello World\"}", schemaUri))
.setReplyChannel(replyChannel)
.build();

SentSubmission sent = onlineService.send(submission);

// Persistieren: sent.caseId() und privateReplyDecryptionKey

Weitere Submissions dem Case zuordnen​

Für alle folgenden Submissions desselben Vorgangs setzen Sie dieselbe caseId:

UUID existingCaseId = sent.caseId(); // aus der Initial-Submission

OutgoingSubmission followUp = OutgoingSubmission.to(recipient)
.inCase(existingCaseId) // <— gleiche caseId wie Initial-Submission
.setData(SubmissionData.json("{\"message\":\"Nachreichung\"}", schemaUri))
.setReplyChannel(replyChannel)
.build();

onlineService.send(followUp);

Replies abholen​

Dieser Abruf läuft typischerweise als eigener Poll-Lauf, ggf. in einem separaten Prozess – deshalb wird der OnlineService hier erneut erzeugt, diesmal mit ReplyKeys zum Entschlüsseln. ReplyKeys ist ein funktionales Interface (Case-ID → Schlüssel) und wird einmal beim Erzeugen übergeben, nicht mehr pro Abruf. Bei mehreren offenen Cases reicht eine Map, ein Vault-Lookup per Methodenreferenz (vault::lookupReplyKey) funktioniert ebenso:

ReplyKeys replyKeys = ReplyKeys.of(Map.of(sent.caseId(), privateReplyDecryptionKey));
OnlineService onlineService = sdk.onlineService(myDestinationId, replyKeys);

Dann abholen und verarbeiten:

RepliesForPickup replies = onlineService.awaitingReplies(/* offset */ 0, /* limit */ 25);

for (ReplyForPickup r : replies.replies()) {
ReceivedReply received = onlineService.receive(r);

String data = received.getDataAsString();
URI schema = received.getDataSchemaUri();

for (Attachment a : received.getAttachments()) {
try (InputStream in = a.openStream()) {
// verarbeiten
}
}

if (received.report().acceptable()) {
onlineService.accept(received);
} else {
onlineService.reject(received, received.report().asProblems());
}
}

Reply akzeptieren oder zurückweisen​

Auch hier gilt das Report-getriebene Muster: received.report().acceptable() (bzw. kurz die Fehlerliste in report().errors()) entscheidet, kein Auto-Reject durch das SDK:

if (received.report().acceptable()) {
// Akzeptieren
onlineService.accept(received);
} else {
// Zurückweisen — Report-Fehler direkt weiterreichen …
onlineService.reject(received, received.report().asProblems());
}

// … oder mit eigenen, zusätzlichen Problem-Typen zurückweisen:
onlineService.reject(received, List.of(
new DataSchemaViolation("Feld X fehlt"),
new MyCustomProblem("Stadtname existiert nicht")));
Nach Accept/Reject: gelöscht

Der Reply wird nach accept-reply oder reject-reply im Zustelldienst dauerhaft gelöscht. Alle benötigten Daten vorher sichern.


Verwaltungssystem — was Sie implementieren​

Typ A/B · Verwaltung

Dieser Abschnitt richtet sich an Entwickler eines Verwaltungssystems (Typ A/B).

Das Verwaltungssystem empfängt Submissions über den normalen Organisation-Weg. Der einzige BiDiKo-spezifische Schritt ist das Persistieren des ReplyChannels und das Senden des Replies.

Reply senden​

Die Empfängerseite baut den Reply auf dem Case der ursprünglichen Submission auf und verschlüsselt ihn mit dem replyEncryptionKey, den der Onlinedienst im ReplyChannel mitgegeben hat.

OutgoingReply.answering(caseId, replyEncryptionKey) baut die Antwort, ohne den ursprünglichen Antrag im Speicher zu halten — ideal, wenn caseId und Schlüssel wie hier aus dem Fachverfahren kommen:

UUID caseId = getCaseIdFromBusinessSystem();
JWK replyEncryptionKey = getReplyEncryptionKeyFromBusinessSystem();

OutgoingReply reply = OutgoingReply.answering(caseId, replyEncryptionKey)
.setData(SubmissionData.json(
replyJson, URI.create("https://schema.example.com/bescheid.json")))
.addAttachment(Attachment.fromFile(
Paths.get("bescheid.pdf"), "application/pdf"))
.build();

Gesendet wird über die eigene Organisation:

Organisation organisation = sdk.organisation(myDestinationId, keys);
SentReply sent = organisation.send(reply);

Haben Sie den Antrag noch als ReceivedSubmission vorliegen, geht es kürzer mit OutgoingReply.answering(received) (Case und Schlüssel werden dann daraus übernommen).

Aktuellsten Schlüssel verwenden

Wenn ein Case mehrere Submissions enthält, kann sich der replyEncryptionKey ändern. Verwenden Sie immer den Schlüssel der neuesten ReceivedSubmission im Case.


Häufige Stolperfallen​

Vollständige Liste
  • caseId oder Schlüssel nicht persistiert: Der Onlinedienst muss caseId und den privaten Schlüssel persistieren. Das Verwaltungssystem muss caseId und den öffentlichen Schlüssel persistieren. Ohne diese Daten können Replies weder entschlüsselt noch gesendet werden.
  • Falscher Prozessstandard: Im ReplyChannel müssen die im Vorgang zulässigen Prozessstandards (z. B. urn:xoev-de:bmk:standard:xbau_2.3) angegeben sein — sonst wird der Reply abgewiesen.
  • Schlüsselrotation im Case: Wenn der Onlinedienst seinen Schlüssel rotiert, muss dies in einer neuen Submission an den Subscriber kommuniziert werden. Es gilt immer der Schlüssel der neuesten Submission im Case.

Weiter geht's​