diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java index 8160139d..90112fe2 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java @@ -5,7 +5,6 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; - import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.check.JsonSchemaValidator; import org.dcsa.conformance.core.scenario.ConformanceAction; @@ -42,10 +41,20 @@ public static LinkedHashMap createModuleScenari .thenEither( initiateAndCloseTransferAction(PintResponseCode.RECE).thenEither( noAction(), - initiateAndCloseTransferAction(PintResponseCode.DUPE)), + retryTransfer(PintResponseCode.DUPE), + manipulateLatestTransactionParameters().then( + retryTransfer(PintResponseCode.DISE) + )), + initiateAndCloseTransferAction(PintResponseCode.RECE, SenderTransmissionClass.VALID_TRANSFER).thenEither( + noAction(), + manipulateLatestTransactionParameters().then( + retryTransfer(PintResponseCode.DISE) + ) + ), initiateAndCloseTransferAction(PintResponseCode.BSIG, SenderTransmissionClass.SIGNATURE_ISSUE).then( initiateAndCloseTransferAction(PintResponseCode.RECE) - ) + ), + initiateAndCloseTransferAction(PintResponseCode.BENV, SenderTransmissionClass.WRONG_RECIPIENT_PLATFORM) ), receiverStateSetup(ScenarioClass.INVALID_RECIPIENT).then( initiateAndCloseTransferAction(PintResponseCode.BENV) @@ -99,6 +108,19 @@ private static PintScenarioListBuilder supplyScenarioParameters(int documentCoun )); } + private static PintScenarioListBuilder manipulateLatestTransactionParameters() { + String sendingPlatform = SENDING_PLATFORM_PARTY_NAME.get(); + String receivingPlatform = RECEIVING_PLATFORM_PARTY_NAME.get(); + return new PintScenarioListBuilder( + previousAction -> + new ManipulateTransactionsAction( + receivingPlatform, + sendingPlatform, + (PintAction) previousAction + )); + } + + private static PintScenarioListBuilder receiverStateSetup(ScenarioClass scenarioClass) { String sendingPlatform = SENDING_PLATFORM_PARTY_NAME.get(); String receivingPlatform = RECEIVING_PLATFORM_PARTY_NAME.get(); @@ -204,7 +226,7 @@ private static PintScenarioListBuilder retryTransfer(PintResponseCode pintRespon private static PintScenarioListBuilder initiateAndCloseTransferAction(PintResponseCode signedResponseCode) { - return initiateAndCloseTransferAction(signedResponseCode, SenderTransmissionClass.VALID); + return initiateAndCloseTransferAction(signedResponseCode, SenderTransmissionClass.VALID_ISSUANCE); } private static PintScenarioListBuilder initiateAndCloseTransferAction(PintResponseCode signedResponseCode, SenderTransmissionClass senderTransmissionClass) { diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ManipulateTransactionsAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ManipulateTransactionsAction.java new file mode 100644 index 00000000..e8c8b860 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ManipulateTransactionsAction.java @@ -0,0 +1,43 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Slf4j +public class ManipulateTransactionsAction extends PintAction { + + public ManipulateTransactionsAction( + String platformPartyName, + String carrierPartyName, + PintAction previousAction) { + super( + carrierPartyName, + platformPartyName, + previousAction, + "ManipulateTransaction", + -1); + } + + @Override + public ObjectNode asJsonNode() { + var node = super.asJsonNode(); + node.set("rsp", getRsp().toJson()); + node.set("ssp", getSsp().toJson()); + node.set("dsp", getDsp().toJson()); + return node; + } + + @Override + public boolean isInputRequired() { + return true; + } + + + @Override + public String getHumanReadablePrompt() { + return ("Manipulate the latest transaction"); + } + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java index c34a9db5..cb91abaf 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java @@ -30,7 +30,7 @@ public PintAction( this.dspReference = new OverwritingReference<>(null, new DynamicScenarioParameters(null, -1, Set.of(), null)); this.rspReference = new OverwritingReference<>(null, new ReceiverScenarioParameters("", "", "", "", "")); - this.sspReference = new OverwritingReference<>(null, new SenderScenarioParameters(null, "")); + this.sspReference = new OverwritingReference<>(null, new SenderScenarioParameters(null, "", "")); } else { this.dspReference = new OverwritingReference<>(previousAction.dspReference, null); this.rspReference = new OverwritingReference<>(previousAction.rspReference, null); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java index 901988a0..ed579c91 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java @@ -61,7 +61,7 @@ public String getHumanReadablePrompt() { @Override public ObjectNode asJsonNode() { var node = super.asJsonNode() - .put("senderTransmissionClass", SenderTransmissionClass.VALID.name()); + .put("senderTransmissionClass", SenderTransmissionClass.VALID_ISSUANCE.name()); node.set("rsp", getRsp().toJson()); node.set("ssp", getSsp().toJson()); node.set("dsp", getDsp().toJson()); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java index 7c8789a5..45080f6d 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java @@ -57,7 +57,7 @@ public String getHumanReadablePrompt() { @Override public ObjectNode asJsonNode() { var node = super.asJsonNode() - .put("senderTransmissionClass", SenderTransmissionClass.VALID.name()); + .put("senderTransmissionClass", SenderTransmissionClass.VALID_ISSUANCE.name()); node.set("rsp", getRsp().toJson()); node.set("ssp", getSsp().toJson()); node.set("dsp", getDsp().toJson()); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAction.java index eb587984..89186de5 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAction.java @@ -17,7 +17,6 @@ import org.dcsa.conformance.core.traffic.ConformanceExchange; import org.dcsa.conformance.core.traffic.HttpMessageType; import org.dcsa.conformance.standards.eblinterop.checks.PintChecks; -import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSignerFactory; import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; import org.dcsa.conformance.standards.eblinterop.party.PintRole; @@ -62,7 +61,7 @@ public String getHumanReadablePrompt() { @Override public ObjectNode asJsonNode() { var node = super.asJsonNode() - .put("senderTransmissionClass", SenderTransmissionClass.VALID.name()); + .put("senderTransmissionClass", SenderTransmissionClass.VALID_ISSUANCE.name()); node.set("rsp", getRsp().toJson()); node.set("ssp", getSsp().toJson()); node.set("dsp", getDsp().toJson()); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAndCloseAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAndCloseAction.java index c426616b..06df1b1b 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAndCloseAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintRetryTransferAndCloseAction.java @@ -43,7 +43,7 @@ public PintRetryTransferAndCloseAction( receivingPlatform, previousAction, "RetryTransfer(%s)".formatted(responseCode.name()), - 200 + responseCode.getHttpResponseCode() ); this.responseCode = responseCode; this.requestSchemaValidator = requestSchemaValidator; @@ -60,7 +60,7 @@ public String getHumanReadablePrompt() { @Override public ObjectNode asJsonNode() { var node = super.asJsonNode() - .put("senderTransmissionClass", SenderTransmissionClass.VALID.name()); + .put("senderTransmissionClass", SenderTransmissionClass.VALID_ISSUANCE.name()); node.set("rsp", getRsp().toJson()); node.set("ssp", getSsp().toJson()); node.set("dsp", getDsp().toJson()); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java index 67e04fc6..acbe969e 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; -import org.dcsa.conformance.standards.eblinterop.models.SenderScenarioParameters; @Getter @Slf4j diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java index 1883b51f..5e4d5868 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java @@ -42,6 +42,7 @@ public void handlePartyInput(JsonNode partyInput) { public JsonNode getJsonForHumanReadablePrompt() { return new SenderScenarioParameters( "TD reference", + "WAVE", "-----BEGIN RSA PUBLIC KEY-----\n\n-----END RSA PUBLIC KEY-----\n" ).toJson(); } diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderTransmissionClass.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderTransmissionClass.java index 9e8be160..55aa0d83 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderTransmissionClass.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderTransmissionClass.java @@ -1,6 +1,8 @@ package org.dcsa.conformance.standards.eblinterop.action; public enum SenderTransmissionClass { - VALID, + VALID_ISSUANCE, + VALID_TRANSFER, SIGNATURE_ISSUE, + WRONG_RECIPIENT_PLATFORM, } diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java index 13277c92..b3978fb4 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java @@ -2,8 +2,10 @@ import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.genericTDContentChecks; +import static org.dcsa.conformance.standards.eblinterop.crypto.SignedNodeSupport.parseSignedNode; import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.JWSObject; @@ -14,6 +16,7 @@ import org.dcsa.conformance.core.traffic.HttpMessageType; import org.dcsa.conformance.standards.ebl.party.TransportDocumentStatus; import org.dcsa.conformance.standards.eblinterop.action.PintResponseCode; +import org.dcsa.conformance.standards.eblinterop.crypto.Checksums; import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; import org.dcsa.conformance.standards.eblinterop.models.DynamicScenarioParameters; import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; @@ -370,6 +373,40 @@ public static ActionCheck validateInitiateTransferRequest( ) ) ); + jsonContentChecks.add( + JsonAttribute.customValidator( + "Validate transfer chain checksums", + JsonAttribute.path("envelopeTransferChain", (etc, contextPath) -> { + String expectedChecksum = null; + if (!etc.isArray()) { + // Leave that to schema validation + return Set.of(); + } + var issues = new LinkedHashSet(); + for (int i = 0 ; i < etc.size() ; i++) { + JsonNode entry = etc.path(i); + JsonNode parsed; + try { + parsed = parseSignedNode(entry); + } catch (ParseException | JsonProcessingException e) { + // Signed content + schema validation already takes care of that issue. + continue; + } + var actualChecksum = parsed.path("previousEnvelopeTransferChainEntrySignedContentChecksum").asText(null); + if (!Objects.equals(expectedChecksum, actualChecksum)) { + var path = contextPath + "[" + i + "].previousEnvelopeTransferChainEntrySignedContentChecksum"; + issues.add("The checksum in '%s' was '%s' but it should have been '%s' (which is the checksum of the preceding item)".formatted( + path, + actualChecksum, + expectedChecksum + )); + } + expectedChecksum = Checksums.sha256(entry.asText()); + } + return issues; + }) + ) + ); return JsonAttribute.contentChecks( PintRole::isSendingPlatform, matched, diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java index 4fe3f6d5..f9d13853 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java @@ -7,17 +7,19 @@ import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; @With -public record SenderScenarioParameters(String transportDocumentReference, String senderPublicKeyPEM) { +public record SenderScenarioParameters(String transportDocumentReference, String eblPlatform, String senderPublicKeyPEM) { public ObjectNode toJson() { return OBJECT_MAPPER .createObjectNode() .put("transportDocumentReference", transportDocumentReference) + .put("eblPlatform", eblPlatform) .put("senderPublicKeyPEM", senderPublicKeyPEM); } public static SenderScenarioParameters fromJson(JsonNode jsonNode) { return new SenderScenarioParameters( jsonNode.required("transportDocumentReference").asText(), + jsonNode.required("eblPlatform").asText(), jsonNode.required("senderPublicKeyPEM").asText() ); } diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java index 13b1df8f..6484a25a 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java @@ -8,10 +8,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.text.ParseException; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.dcsa.conformance.core.state.JsonNodeMap; @@ -20,6 +17,7 @@ import org.dcsa.conformance.core.traffic.ConformanceResponse; import org.dcsa.conformance.standards.eblinterop.action.PintResponseCode; import org.dcsa.conformance.standards.eblinterop.action.ScenarioClass; +import org.dcsa.conformance.standards.eblinterop.crypto.Checksums; import org.dcsa.conformance.standards.eblinterop.crypto.CouldNotValidateSignatureException; import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSignerFactory; import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; @@ -38,6 +36,10 @@ public class TDReceiveState { private static final String EXPECTED_PUBLIC_KEY = "expectedSenderPublicKey"; + private static final String RECEIVING_PLATFORM = "receivingEBLPlatform"; + + private static final String TRANSFER_CHAIN_ENTRY_HISTORY = "transferChainEntryHistory"; + private final ObjectNode state; private TDReceiveState(ObjectNode state) { @@ -114,6 +116,24 @@ public String envelopeReference() { return envelopReference; } + private boolean isEnvelopeTransferChainValid(JsonNode etc) { + String expectedChecksum = null; + for (JsonNode entry : etc) { + JsonNode parsed; + try { + parsed = parseSignedNode(entry); + } catch (ParseException | JsonProcessingException e) { + return false; + } + var actualChecksum = parsed.path("previousEnvelopeTransferChainEntrySignedContentChecksum").asText(null); + if (!Objects.equals(expectedChecksum, actualChecksum)) { + return false; + } + expectedChecksum = Checksums.sha256(entry.asText()); + } + return true; + } + public PintResponseCode recommendedFinishTransferResponse(JsonNode initiateRequest, SignatureVerifier signatureVerifer) { var etc = initiateRequest.path("envelopeTransferChain"); var lastEtcEntry = etc.path(etc.size() - 1); @@ -126,12 +146,38 @@ public PintResponseCode recommendedFinishTransferResponse(JsonNode initiateReque } catch (CouldNotValidateSignatureException e) { return PintResponseCode.BSIG; } + if (!isEnvelopeTransferChainValid(etc)) { + return PintResponseCode.BENV; + } var transactions = etcEntryParsed.path("transactions"); var lastTransactionNode = transactions.path(transactions.size() - 1); var recipient = lastTransactionNode.path("recipient"); var recipientPartyCodes = recipient.path("partyCodes"); + var recipientPlatform = recipient.path("eblPlatform").asText("!?"); var expectedReceiver = state.path(EXPECTED_RECEIVER).asText(); boolean hasExpectedCode = false; + var transferChainEntryHistory = this.state.path(TRANSFER_CHAIN_ENTRY_HISTORY); + for (int i = 0; i < transferChainEntryHistory.size() ; i++) { + var transferChainEntry = etc.path(i); + var expectedTransferChainEntry = transferChainEntryHistory.path(i); + if (expectedTransferChainEntry.isNull()) { + // We allow a re-transfer with same history + if (!transferChainEntry.isMissingNode()) { + return PintResponseCode.DISE; + } + break; + } + if (!Objects.equals(expectedTransferChainEntry.asText(), transferChainEntry.asText())) { + return PintResponseCode.DISE; + } + } + if (transferChainEntryHistory.size() < transactions.size()) { + var newTransactionHistory = this.state.putArray(TRANSFER_CHAIN_ENTRY_HISTORY); + for (var transaction : etc) { + newTransactionHistory.add(transaction); + } + } + for (var partyCodeNode : recipientPartyCodes) { if (!partyCodeNode.path("codeListProvider").asText("").equals("EPUI")) { continue; @@ -142,6 +188,10 @@ public PintResponseCode recommendedFinishTransferResponse(JsonNode initiateReque } } + if (!state.path(RECEIVING_PLATFORM).asText("?").equals(recipientPlatform)) { + hasExpectedCode = false; + } + if (!hasExpectedCode) { return PintResponseCode.BENV; } @@ -165,6 +215,15 @@ public PintResponseCode finishTransferCode() { if (!getMissingDocumentChecksums().isEmpty()) { return PintResponseCode.MDOC; } + var newTransferHistory = this.state.path(TRANSFER_CHAIN_ENTRY_HISTORY); + var lastEntry = newTransferHistory.path(newTransferHistory.size() - 1); + if (!lastEntry.isNull()) { + if (!newTransferHistory.isArray()) { + newTransferHistory = this.state.putArray(TRANSFER_CHAIN_ENTRY_HISTORY); + } + // We use the null node to mark it as us owning the eBL. + ((ArrayNode)newTransferHistory).addNull(); + } if (this.getTransferState() == TransferState.ACCEPTED) { responseCode = PintResponseCode.DUPE; } @@ -189,10 +248,11 @@ public static TDReceiveState fromPersistentStore(JsonNode state) { return new TDReceiveState((ObjectNode) state); } - public static TDReceiveState newInstance(String transportDocumentReference, String senderPublicKeyPEM) { + public static TDReceiveState newInstance(String transportDocumentReference, String senderPublicKeyPEM, ReceiverScenarioParameters receivingParameters) { var state = OBJECT_MAPPER.createObjectNode() .put(TRANSPORT_DOCUMENT_REFERENCE, transportDocumentReference) .put(TRANSFER_STATE, TransferState.NOT_STARTED.name()) + .put(RECEIVING_PLATFORM, receivingParameters.eblPlatform()) .put(EXPECTED_PUBLIC_KEY, senderPublicKeyPEM); return new TDReceiveState(state); } diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDSendingState.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDSendingState.java index 7e5bd1e1..5757f0d3 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDSendingState.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDSendingState.java @@ -1,16 +1,20 @@ package org.dcsa.conformance.standards.eblinterop.models; import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.dcsa.conformance.standards.eblinterop.crypto.SignedNodeSupport.parseSignedNode; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BinaryNode; import com.fasterxml.jackson.databind.node.ObjectNode; - +import java.time.Instant; +import java.util.Map; import java.util.Objects; import java.util.UUID; - +import lombok.SneakyThrows; import org.dcsa.conformance.core.state.JsonNodeMap; import org.dcsa.conformance.standards.eblinterop.crypto.Checksums; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSigner; public class TDSendingState { @@ -19,6 +23,21 @@ public class TDSendingState { private static final String SIGNED_MANIFEST = "signedManifest"; private static final String ENVELOPE_TRANSFER_CHAIN = "envelopeTransferChain"; + + private static final Map PLATFORM2CODELISTNAME = Map.ofEntries( + Map.entry("WAVE", "Wave"), + Map.entry("CARX", "CargoX"), + Map.entry("EDOX", "EdoxOnline"), + Map.entry("IQAX", "IQAX"), + Map.entry("ESSD", "EssDOCS"), + Map.entry("BOLE", "Bolero"), + Map.entry("TRGO", "TradeGO"), + Map.entry("SECR", "Secro")/*, + Map.entry("", "GSBN"), + Map.entry("", "WiseTech") + */ + ); + private final ObjectNode state; private TDSendingState(ObjectNode state) { @@ -95,6 +114,91 @@ public JsonNode getSignedEnvelopeTransferChain() { + public static String platform2CodeListName(String platform) { + // The default is not valid, but it only happens with unknown platforms and null would be even worse + return PLATFORM2CODELISTNAME.getOrDefault(platform, platform); + } + + public static ObjectNode generateTransaction(String action, String sendingPlatform, String sendingLegalName, String sendingEPUI, String receivingPlatform, String receivingLegalName, String receivingEPUI, String receivingCodeListName) { + var actor = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", sendingPlatform) + .put("legalName", sendingLegalName); + actor.putArray("partyCodes") + .addObject() + .put("partyCode", sendingEPUI) + .put("codeListProvider", "EPUI") + .put("codeListName", platform2CodeListName(sendingPlatform)); + var recipient = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", receivingPlatform) + .put("legalName", receivingLegalName); + recipient.putArray("partyCodes") + .addObject() + .put("partyCode", receivingEPUI) + .put("codeListProvider", "EPUI") + .put("codeListName", receivingCodeListName); + var transaction = OBJECT_MAPPER.createObjectNode() + .put("action", action) + .put("timestamp", Instant.now().toEpochMilli()); + transaction.set("actor", actor); + transaction.set("recipient", recipient); + return transaction; + } + + public static String generateTransactionEntry(PayloadSigner payloadSigner, String previousEnvelopeTransferChainEntrySignedContentChecksum, String tdChecksum, String action, String sendingPlatform, String sendingLegalName, String sendingEPUI, String receivingPlatform, String receivingLegalName, String receivingEPUI, String receivingCodeListName) { + var latestEnvelopeTransferChainUnsigned = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", sendingPlatform) + .put("transportDocumentChecksum", tdChecksum) + .put("previousEnvelopeTransferChainEntrySignedContentChecksum", previousEnvelopeTransferChainEntrySignedContentChecksum); + + latestEnvelopeTransferChainUnsigned + .putArray("transactions") + .add(generateTransaction( + action, + sendingPlatform, + sendingLegalName, + sendingEPUI, + receivingPlatform, + receivingLegalName, + receivingEPUI, + receivingCodeListName + )); + + + return payloadSigner.sign(latestEnvelopeTransferChainUnsigned.toString()); + } + + @SneakyThrows + public void manipulateLatestTransaction(PayloadSigner payloadSigner, ReceiverScenarioParameters rsp) { + var chain = this.state.path(ENVELOPE_TRANSFER_CHAIN); + var lastSigned = chain.path(chain.size() - 1); + var last = parseSignedNode(lastSigned); + var sendingPlatform = "BOLE"; + var receivingPlatform = rsp.eblPlatform(); + var sendingEPUI = "1234"; + var sendingLegalName = "DCSA CTK tester"; + var receivingEPUI = rsp.receiverEPUI(); + var receivingLegalName = rsp.receiverLegalName(); + var receiverCodeListName = rsp.receiverEPUICodeListName(); + if (sendingPlatform.equals(receivingPlatform)) { + sendingPlatform = "WAVE"; + } + var newTransactionEntry = generateTransaction( + "TRNS", + sendingPlatform, + sendingLegalName, + sendingEPUI, + sendingPlatform, + receivingLegalName, + receivingEPUI, + receiverCodeListName + ); + + ((ArrayNode)last.path("transactions")).insert(0, newTransactionEntry); + + var newSigned = payloadSigner.sign(last.toString()); + ((ArrayNode)chain).set(chain.size() - 1, newSigned); + } + public ObjectNode generateEnvelopeManifest(String transportDocumentChecksum, String lastEnvelopeTransferChainEntrySignedContentChecksum) { var unsignedEnvelopeManifest = OBJECT_MAPPER.createObjectNode() .put("transportDocumentChecksum", transportDocumentChecksum) diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java index 36c841bc..f79293d2 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java @@ -95,18 +95,10 @@ private void initiateState(JsonNode actionPrompt) { if (existing != null){ throw new IllegalStateException("Please do not reuse TDRs between scenarios in the conformance test"); } - var scenarioClass = ScenarioClass.valueOf(actionPrompt.required("scenarioClass").asText()); var expectedRecipient = "12345-jane-doe"; - var receivingParameters = new ReceiverScenarioParameters( - "CARX", - "Jane Doe", - scenarioClass == ScenarioClass.INVALID_RECIPIENT ? "12345-invalid" : expectedRecipient, - "CargoX", - PayloadSignerFactory.receiverKeySignatureVerifier().getPublicKeyInPemFormat() - ); - - var tdState = TDReceiveState.newInstance(tdr, ssp.senderPublicKeyPEM()); + var receivingParameters = getReceiverScenarioParameters(ssp, scenarioClass, expectedRecipient); + var tdState = TDReceiveState.newInstance(tdr, ssp.senderPublicKeyPEM(), receivingParameters); tdState.setExpectedReceiver(expectedRecipient); tdState.setScenarioClass(scenarioClass); tdState.save(persistentMap); @@ -119,6 +111,24 @@ private void initiateState(JsonNode actionPrompt) { "Finished ScenarioType"); } + private static ReceiverScenarioParameters getReceiverScenarioParameters(SenderScenarioParameters ssp, ScenarioClass scenarioClass, String expectedRecipient) { + String platform, codeListName; + if ("CARX".equals(ssp.eblPlatform())) { + platform = "BOLE"; + codeListName = "Bolero"; + } else { + platform = "CARX"; + codeListName = "CargoX"; + } + return new ReceiverScenarioParameters( + platform, + "Jane Doe", + scenarioClass == ScenarioClass.INVALID_RECIPIENT ? "12345-invalid" : expectedRecipient, + codeListName, + PayloadSignerFactory.receiverKeySignatureVerifier().getPublicKeyInPemFormat() + ); + } + public ConformanceResponse handleInitiateTransferRequest(ConformanceRequest request) { var transferRequest = request.message().body().getJsonBody(); diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java index 68558621..58cb2c67 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java @@ -1,12 +1,15 @@ package org.dcsa.conformance.standards.eblinterop.party; import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.dcsa.conformance.standards.eblinterop.action.SenderTransmissionClass.*; +import static org.dcsa.conformance.standards.eblinterop.models.TDSendingState.generateTransactionEntry; +import static org.dcsa.conformance.standards.eblinterop.models.TDSendingState.platform2CodeListName; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.nimbusds.jose.util.Base64URL; -import java.time.Instant; + import java.util.*; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; @@ -34,19 +37,6 @@ public class PintSendingPlatform extends ConformanceParty { private static final Random RANDOM = new Random(); - private static final Map PLATFORM2CODELISTNAME = Map.ofEntries( - Map.entry("WAVE", "Wave"), - Map.entry("CARX", "CargoX"), - Map.entry("EDOX", "EdoxOnline"), - Map.entry("IQAX", "IQAX"), - Map.entry("ESSD", "EssDOCS"), - Map.entry("BOLE", "Bolero"), - Map.entry("TRGO", "TradeGO"), - Map.entry("SECR", "Secro")/*, - Map.entry("", "GSBN"), - Map.entry("", "WiseTech") - */ - ); private final PayloadSigner payloadSigner; @@ -82,9 +72,10 @@ protected void doReset() {} protected Map, Consumer> getActionPromptHandlers() { return Map.ofEntries( Map.entry(SenderSupplyScenarioParametersAction.class, this::supplyScenarioParameters), - Map.entry(PintInitiateAndCloseTransferAction.class, this::sendIssuanceRequest), - Map.entry(PintInitiateTransferAction.class, this::sendIssuanceRequest), - Map.entry(PintInitiateTransferUnsignedErrorAction.class, this::sendIssuanceRequest), + Map.entry(PintInitiateAndCloseTransferAction.class, this::initiateTransferRequest), + Map.entry(PintInitiateTransferAction.class, this::initiateTransferRequest), + Map.entry(PintInitiateTransferUnsignedErrorAction.class, this::initiateTransferRequest), + Map.entry(ManipulateTransactionsAction.class, this::manipulateTransactions), Map.entry(PintTransferAdditionalDocumentAction.class, this::transferActionDocument), Map.entry(PintRetryTransferAction.class, this::retryTransfer), Map.entry(PintRetryTransferAndCloseAction.class, this::retryTransfer), @@ -138,7 +129,7 @@ private String generateTDR() { private void supplyScenarioParameters(JsonNode actionPrompt) { log.info("EblInteropSendingPlatform.supplyScenarioParameters(%s)".formatted(actionPrompt.toPrettyString())); var tdr = generateTDR(); - var scenarioParameters = new SenderScenarioParameters(tdr, PayloadSignerFactory.senderKeySignatureVerifier().getPublicKeyInPemFormat()); + var scenarioParameters = new SenderScenarioParameters(tdr, "BOLE", PayloadSignerFactory.senderKeySignatureVerifier().getPublicKeyInPemFormat()); asyncOrchestratorPostPartyInput( OBJECT_MAPPER .createObjectNode() @@ -148,6 +139,25 @@ private void supplyScenarioParameters(JsonNode actionPrompt) { "Provided ScenarioParameters: %s".formatted(scenarioParameters)); } + private void manipulateTransactions(JsonNode actionPrompt) { + log.info( + "EblInteropSendingPlatform.manipulateTransactions(%s)" + .formatted(actionPrompt.toPrettyString())); + var rsp = ReceiverScenarioParameters.fromJson(actionPrompt.required("rsp")); + var ssp = SenderScenarioParameters.fromJson(actionPrompt.required("ssp")); + var tdr = ssp.transportDocumentReference(); + var sendingState = TDSendingState.load(persistentMap, tdr); + sendingState.manipulateLatestTransaction(payloadSigner, rsp); + sendingState.save(persistentMap); + asyncOrchestratorPostPartyInput( + OBJECT_MAPPER + .createObjectNode() + .put("actionId", actionPrompt.required("actionId").asText()) + .putNull("input")); + addOperatorLogEntry( + "Mutated transaction chain for document: %s".formatted(tdr)); + } + private void transferActionDocument(JsonNode actionPrompt) { log.info("EblInteropSendingPlatform.transferActionDocument(%s)".formatted(actionPrompt.toPrettyString())); var dsp = DynamicScenarioParameters.fromJson(actionPrompt.required("dsp")); @@ -218,11 +228,69 @@ private ObjectNode loadTDR(String tdr) { return tdPayload.put("transportDocumentReference", tdr); } - private void sendIssuanceRequest(JsonNode actionPrompt) { + private String generateTransactions(ObjectNode payload, String tdChecksum, SenderTransmissionClass senderTransmissionClass, ReceiverScenarioParameters rsp) { + var sendingPlatform = "BOLE"; + var receivingPlatform = rsp.eblPlatform(); + var sendingEPUI = "1234"; + var sendingLegalName = "DCSA CTK tester"; + var receivingEPUI = rsp.receiverEPUI(); + var receivingLegalName = rsp.receiverLegalName(); + var receiverCodeListName = rsp.receiverEPUICodeListName(); + if (sendingPlatform.equals(receivingPlatform)) { + sendingPlatform = "WAVE"; + } + var transactions = payload.putArray("envelopeTransferChain"); + var action = "ISSU"; + String previousChecksum = null; + if (senderTransmissionClass == VALID_TRANSFER) { + var codeListName = platform2CodeListName(sendingPlatform); + var transaction = generateTransactionEntry( + payloadSigner, + null, + tdChecksum, + action, + sendingPlatform, + "DCSA CTK issuer", + "5432", + sendingPlatform, + sendingLegalName, + sendingEPUI, + codeListName + ); + previousChecksum = Checksums.sha256(transaction); + transactions.add(transaction); + action = "TRNS"; + } + if (senderTransmissionClass == WRONG_RECIPIENT_PLATFORM) { + if (receivingPlatform.equals("WAVE")) { + receivingPlatform = "BOLE"; + } else { + receivingPlatform = "WAVE"; + } + } + var latest = generateTransactionEntry( + payloadSigner, + previousChecksum, + tdChecksum, + action, + sendingPlatform, + sendingLegalName, + sendingEPUI, + receivingPlatform, + receivingLegalName, + receivingEPUI, + receiverCodeListName + ); + transactions.add(latest); + return latest; + } + + private void initiateTransferRequest(JsonNode actionPrompt) { log.info("EblInteropSendingPlatform.sendIssuanceRequest(%s)".formatted(actionPrompt.toPrettyString())); var dsp = DynamicScenarioParameters.fromJson(actionPrompt.required("dsp")); var ssp = SenderScenarioParameters.fromJson(actionPrompt.required("ssp")); var tdr = ssp.transportDocumentReference(); + var senderTransmissionClass = SenderTransmissionClass.valueOf(actionPrompt.required("senderTransmissionClass").asText()); boolean isCorrect = actionPrompt.path("isCorrect").asBoolean(); @@ -236,54 +304,28 @@ private void sendIssuanceRequest(JsonNode actionPrompt) { if (!isCorrect && tdPayload.path("transportDocument").has("issuingParty")) { ((ObjectNode) tdPayload.path("transportDocument")).remove("issuingParty"); } + var rsp = ReceiverScenarioParameters.fromJson(actionPrompt.required("rsp")); var tdChecksum = Checksums.sha256CanonicalJson(tdPayload); - var sendingPlatform = "BOLE"; - var receivingPlatform = rsp.eblPlatform(); - var sendingEPUI = "1234"; - var sendingLegalName = "DCSA CTK tester"; - var receivingEPUI = rsp.receiverEPUI(); - var receivingLegalName = rsp.receiverLegalName(); - var receiverCodeListName = rsp.receiverEPUICodeListName(); - var latestEnvelopeTransferChainUnsigned = OBJECT_MAPPER.createObjectNode() - .put("eblPlatform", sendingPlatform) - .put("transportDocumentChecksum", tdChecksum) - .putNull("previousEnvelopeTransferChainEntrySignedContentChecksum"); - var issuingParty = OBJECT_MAPPER.createObjectNode() - .put("eblPlatform", sendingPlatform) - .put("legalName", sendingLegalName); - issuingParty.putArray("partyCodes") - .addObject() - .put("partyCode", sendingEPUI) - .put("codeListProvider", "EPUI") - .put("codeListName", PLATFORM2CODELISTNAME.get(sendingPlatform)); - var issueToParty = OBJECT_MAPPER.createObjectNode() - .put("eblPlatform", receivingPlatform) - .put("legalName", receivingLegalName); - issueToParty.putArray("partyCodes") - .addObject() - .put("partyCode", receivingEPUI) - .put("codeListProvider", "EPUI") - .put("codeListName", receiverCodeListName); - var transaction = latestEnvelopeTransferChainUnsigned - .putArray("transactions") - .addObject() - .put("action", "ISSU") - .put("timestamp", Instant.now().toEpochMilli()); - transaction.set("actor", issuingParty); - transaction.set("recipient", issueToParty); - - var latestEnvelopeTransferChainEntrySigned = payloadSigner.sign(latestEnvelopeTransferChainUnsigned.toString()); - var unsignedEnvelopeManifest = sendingState.generateEnvelopeManifest(tdChecksum, Checksums.sha256(latestEnvelopeTransferChainEntrySigned)); + + var latestEnvelopeTransferChainEntrySigned = generateTransactions( + body, + tdChecksum, + senderTransmissionClass, + rsp + ); + var unsignedEnvelopeManifest = sendingState.generateEnvelopeManifest( + tdChecksum, + Checksums.sha256(latestEnvelopeTransferChainEntrySigned) + ); JsonNode signedManifest = TextNode.valueOf(payloadSigner.sign(unsignedEnvelopeManifest.toString())); sendingState.setSignedManifest(signedManifest); - if (senderTransmissionClass == SenderTransmissionClass.SIGNATURE_ISSUE) { + if (senderTransmissionClass == SIGNATURE_ISSUE) { signedManifest = mutatePayload(signedManifest); } body.set("envelopeManifestSignedContent", signedManifest); - var envelopeTransferChain = body.putArray("envelopeTransferChain") - .add(latestEnvelopeTransferChainEntrySigned); + var envelopeTransferChain = body.path("envelopeTransferChain"); sendingState.setSignedEnvelopeTransferChain(envelopeTransferChain); sendingState.save(persistentMap); var response = this.syncCounterpartPost(