Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fit-connect/sdk-java
1 result
Show changes
Showing
with 882 additions and 264 deletions
package dev.fitko.fitconnect.api.domain.validation;
import dev.fitko.fitconnect.api.domain.model.event.problems.Problem;
import java.util.ArrayList;
import java.util.List;
/**
* Wrapper for validations including an exception
*/
public final class ValidationResult {
private final boolean isValid;
private Exception error;
private final Exception error;
private final List<Problem> validationProblems = new ArrayList<>();
private ValidationResult(final boolean isValid) {
this.isValid = isValid;
error = null;
}
private ValidationResult(final boolean isValid, final Exception exception) {
private ValidationResult(final boolean isValid, final Exception error) {
this.isValid = isValid;
this.error = exception;
this.error = error;
}
private ValidationResult(final Problem problem) {
isValid = false;
error = null;
validationProblems.add(problem);
}
private ValidationResult(final List<Problem> problems) {
isValid = false;
error = null;
validationProblems.addAll(problems);
}
private ValidationResult(final Exception error, final Problem problem) {
isValid = false;
this.error = error;
validationProblems.add(problem);
}
/**
* Create new valid result.
*
* @return the valid result
*/
public static ValidationResult ok() {
return new ValidationResult(true);
}
/**
* Create new failed result with an exception.
*
* @return the invalid result
*/
public static ValidationResult error(final Exception exception) {
return new ValidationResult(false, exception);
}
/**
* Create new failed result with a {@link Problem}.
*
* @return the invalid result
*/
public static ValidationResult problem(final Problem problem) {
return new ValidationResult(problem);
}
/**
* Create new failed result with a list of {@link Problem}.
*
* @return the invalid result
*/
public static ValidationResult problems(final List<Problem> problems) {
return new ValidationResult(problems);
}
/**
* Create new failed result with an Exception and a {@link Problem}.
*
* @return the invalid result
*/
public static ValidationResult withErrorAndProblem(final Exception exception, final Problem problem) {
return new ValidationResult(exception, problem);
}
/**
* Successful validation without errors.
*
* @return true if valid
*/
public boolean isValid() {
return this.isValid;
return isValid;
}
/**
* Failed validation with an error.
*
* @return true if an error occurred
*/
public boolean hasError() {
return !this.isValid || this.error != null;
return error != null;
}
/**
* Failed validation with a problem error that gets auto-rejected.
*
* @return true if a problem occurred
*/
public boolean hasProblems() {
return !validationProblems.isEmpty();
}
/**
* Gets the problem that was detected during validation.
*
* @return {@link Problem}
*/
public List<Problem> getProblems() {
return validationProblems;
}
/**
* Gets the exception that occurred during validation.
*
* @return {@link Exception}
*/
public Exception getError() {
return this.error;
return error;
}
}
package dev.fitko.fitconnect.api.exceptions;
public class AuthenticationTagsEmptyException extends RuntimeException {
public AuthenticationTagsEmptyException(final String errorMessage, final Throwable error) {
super(errorMessage, error);
}
public AuthenticationTagsEmptyException(final String errorMessage) {
super(errorMessage);
}
}
package dev.fitko.fitconnect.api.exceptions;
import lombok.Getter;
import org.springframework.http.HttpStatus;
public class RestApiException extends RuntimeException {
@Getter
private HttpStatus httpStatus;
public RestApiException(final String errorMessage, final Throwable error) {
super(errorMessage, error);
}
......@@ -9,4 +15,9 @@ public class RestApiException extends RuntimeException {
public RestApiException(final String errorMessage) {
super(errorMessage);
}
public RestApiException(final HttpStatus httpStatus, final String errorMessage) {
super(errorMessage);
this.httpStatus = httpStatus;
}
}
package dev.fitko.fitconnect.api.exceptions;
public class SubmissionRequestException extends RuntimeException {
public SubmissionRequestException(final String errorMessage, final Throwable error) {
super(errorMessage, error);
}
public SubmissionRequestException(final String errorMessage) {
super(errorMessage);
}
}
package dev.fitko.fitconnect.api.exceptions;
public class SubmitEventNotFoundException extends RuntimeException {
public SubmitEventNotFoundException(final String errorMessage, final Throwable error) {
super(errorMessage, error);
}
public SubmitEventNotFoundException(final String errorMessage) {
super(errorMessage);
}
}
......@@ -28,21 +28,6 @@ import java.util.UUID;
*/
public interface Sender {
/**
* Validates the public key consisting of the following steps:
* <p>
* <ul>
* <li>checks if the JSON Web Key is suitable for the encryption</li>
* <li>checks if the public key is matching the certificate referenced in the JWK</li>
* <li>checks the certificate chain up to the root certificate</li>
* <li>checks against a certificate revocation list and/or an OSCP-endpoint with signed response</li>
* </ul>
* @param publicKey the public JWK
*
* @return {@link ValidationResult} that includes an error if the validation failed
*/
ValidationResult validatePublicKey(RSAKey publicKey);
/**
* Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness.
*
......
......@@ -2,14 +2,19 @@ package dev.fitko.fitconnect.api.services;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
import dev.fitko.fitconnect.api.domain.model.event.EventPayload;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.DecryptionException;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import java.util.List;
import java.util.Set;
......@@ -29,7 +34,7 @@ public interface Subscriber {
* @param encryptedContent JWE encrypted content that should be decrypted
* @return the decrypted content as byte[]
*/
byte[] decryptStringContent(RSAKey privateKey, String encryptedContent);
byte[] decryptStringContent(RSAKey privateKey, String encryptedContent) throws DecryptionException;
/**
* Polls available {@link SubmissionForPickup}s for a given destinationId.
......@@ -39,7 +44,7 @@ public interface Subscriber {
* @param offset position in the dataset
* @return list of found {@link SubmissionForPickup}s
*/
Set<SubmissionForPickup> pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit);
Set<SubmissionForPickup> pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit) throws RestApiException;
/**
* Polls available {@link SubmissionForPickup}s for all destinations the {@link Subscriber} is assigned to.
......@@ -56,7 +61,7 @@ public interface Subscriber {
* @param submissionId the unique identifier of a {@link Submission}
* @return the requested {@link Submission}
*/
Submission getSubmission(UUID submissionId);
Submission getSubmission(UUID submissionId) throws RestApiException;
/**
* Loads encrypted {@link ApiAttachment} for a {@link Submission}.
......@@ -65,7 +70,7 @@ public interface Subscriber {
* @param attachmentId unique identifier of the attachments
* @return encrypted JWE string of attachment
*/
String fetchAttachment(UUID submissionId, UUID attachmentId);
String fetchAttachment(UUID submissionId, UUID attachmentId) throws RestApiException;
/**
* Retrieve the entire event log for a submissions caseId of a specific destination.
......@@ -74,15 +79,38 @@ public interface Subscriber {
* @param destinationId unique identifier of the {@link Destination} the log should be retrieved for
* @return List of {@link EventLogEntry}s for the given case
*/
List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId);
List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws RestApiException;
/**
* Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness.
* Validates the {@link Metadata} structure and contents to ensure its correctness.
*
* @param metadata the {@link Metadata} object that is validated
* @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid or doesn't match the schema
* @param submission the {@link Submission} of the validated metadata
* @param authenticationTags the {@link AuthenticationTags} of the validated metadata
* @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema
*/
ValidationResult validateMetadata(Metadata metadata);
ValidationResult validateMetadata(Metadata metadata, Submission submission, AuthenticationTags authenticationTags);
/**
* Validates the attachment structure and contents to ensure its correctness.
*
* @param attachmentsForValidation list of attachments containing the hash, decrypted and encrypted data needed for validation
* @param authenticationTags the {@link AuthenticationTags} of the validated attachments
* @return a {@link ValidationResult}, contains an error if the attachment is invalid
*/
ValidationResult validateAttachments(List<AttachmentForValidation> attachmentsForValidation, AuthenticationTags authenticationTags);
/**
* Validates the {@link Data} structure and contents to ensure its correctness.
*
* @param data the unencrypted data as byte[]
* @param submission the {@link Submission} of the validated data
* @param metadata the {@link Metadata} of the validated data
* @param authenticationTags the {@link AuthenticationTags} of the validated data
* @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema
*/
ValidationResult validateData(byte[] data, Submission submission, Metadata metadata, AuthenticationTags authenticationTags);
/**
* Validates data integrity of {@link ApiAttachment} or {@link Data}.
......@@ -119,4 +147,13 @@ public interface Subscriber {
* @see <a href="https://docs.fitko.de/fit-connect/docs/receiving/process-and-acknowledge">Process And Acknowledge</a>
*/
void rejectSubmission(EventPayload eventPayload);
/**
* Get event from event-log filtered by a given event and submission.
*
* @param event event type to filter the event-log by
* @param submission submission to filter the event-log by
* @return {@link AuthenticationTags} of metadata, data and attachments
*/
AuthenticationTags getAuthenticationTagsForEvent(Event event, Submission submission);
}
package dev.fitko.fitconnect.api.services.events;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventLog;
import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.exceptions.EventLogException;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import java.util.List;
import java.util.UUID;
......@@ -27,8 +27,9 @@ public interface EventLogService {
* @param caseId unique case identifier
* @param destinationId unique identifier of the destination
* @return list of {@link EventLogEntry}
* @throws EventLogException if a technical error occurred or the validation failed
*/
List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws RestApiException, EventLogException;
List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws EventLogException;
/**
* Retrieve the current status of a {@link Submission}.
......@@ -37,16 +38,27 @@ public interface EventLogService {
* @param caseId unique identifier of the case the log should be retrieved for
* @param submissionId unique identifier of the submission the log should be retrieved for
* @param authenticationTags {@link AuthenticationTags} used for SET-Event integrity validation
*
* @return {@link SubmissionStatus} the current status
* @throws EventLogException if a technical error occurred or the validation failed
*/
SubmissionStatus getLastedEvent(UUID destinationId, UUID caseId, UUID submissionId, AuthenticationTags authenticationTags) throws RestApiException, EventLogException;
SubmissionStatus getLastedEvent(UUID destinationId, UUID caseId, UUID submissionId, AuthenticationTags authenticationTags) throws EventLogException;
/**
* Send an event for a given caseId.
*
* @param caseId unique case identifier
* @param signedAndSerializedSET the serialised and signed event as SET string
* @throws EventLogException if a technical error occurred
*/
void sendEvent(UUID caseId, String signedAndSerializedSET) throws EventLogException;
/**
* Get authentication tags for a given event and caseId.
*
* @param event the event type to filter the log for
* @param submission submission data
* @return {@link AuthenticationTags} for metadata, data and attachments
* @throws EventLogException if a technical error occurred or the validation failed
*/
void sendEvent(UUID caseId, String signedAndSerializedSET) throws RestApiException;
AuthenticationTags getAuthenticationTagsForEvent(Event event, Submission submission) throws EventLogException;
}
package dev.fitko.fitconnect.api.services.keys;
import com.nimbusds.jose.jwk.KeyOperation;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.services.validation.ValidationService;
......@@ -19,7 +20,7 @@ public interface KeyService {
*
* @param destinationId unique identifier of the {@link Destination}
*
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
* @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}})
*/
RSAKey getPublicEncryptionKey(UUID destinationId);
......@@ -28,7 +29,7 @@ public interface KeyService {
*
* @param destinationId unique identifier of the {@link Destination}
* @param keyId unique identifier of the {@link RSAKey}
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
* @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}})
*/
RSAKey getPublicSignatureKey(UUID destinationId, String keyId);
......@@ -36,7 +37,7 @@ public interface KeyService {
* Get a public signature key for a given key-id from the self-service portal well-known keys.
*
* @param keyId unique identifier of the {@link RSAKey}
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
* @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}})
*/
RSAKey getPortalPublicKey(String keyId);
......@@ -44,7 +45,7 @@ public interface KeyService {
* Get a public signature key for a given key-id from the submission service well-known keys.
*
* @param keyId unique identifier of the {@link RSAKey}
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
* @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}})
*/
RSAKey getSubmissionServicePublicKey(String keyId);
......@@ -54,7 +55,7 @@ public interface KeyService {
*
* @param url custom url to load the well known keys from
* @param keyId unique identifier of the {@link RSAKey} the well known keys are filtered by
* @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
* @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}})
*/
RSAKey getWellKnownKeysForSubmissionUrl(String url, String keyId);
}
......@@ -30,7 +30,7 @@ public interface SubmissionService {
*
* @see <a href="https://docs.fitko.de/fit-connect/docs/sending/start-submission">Announcing a submission</a>
*/
SubmissionForPickup announceSubmission(CreateSubmission submission);
SubmissionForPickup announceSubmission(CreateSubmission submission) throws RestApiException;
/**
* Send a submission that was already {@link #announceSubmission(CreateSubmission) announced} before.
......@@ -38,7 +38,7 @@ public interface SubmissionService {
* @param submission submission including the encrypted {@link Data} and {@link Metadata}
* @return the submission that was sent
*/
Submission sendSubmission(SubmitSubmission submission);
Submission sendSubmission(SubmitSubmission submission) throws RestApiException;
/**
* Get a {@link Submission} by id.
......@@ -46,7 +46,7 @@ public interface SubmissionService {
* @param submissionId unique submission identifier
* @return submission matching the given id
*/
Submission getSubmission(UUID submissionId);
Submission getSubmission(UUID submissionId) throws RestApiException;
/**
* Get all available submissions.
......@@ -65,7 +65,7 @@ public interface SubmissionService {
* @param limit number of submissions in result (max. is 500)
* @return SubmissionsForPickup containing a list of submissions
*/
SubmissionsForPickup pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit);
SubmissionsForPickup pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit) throws RestApiException;
/**
* Upload an encrypted {@link ApiAttachment}.
......@@ -74,7 +74,7 @@ public interface SubmissionService {
* @param attachmentId unique destination identifier
* @param encryptedAttachment JWE encrypted attachment payload
*/
void uploadAttachment(UUID submissionId, UUID attachmentId, String encryptedAttachment);
void uploadAttachment(UUID submissionId, UUID attachmentId, String encryptedAttachment) throws RestApiException;
/**
* Get an {@link ApiAttachment} by id for a given {@link Submission}.
......@@ -83,7 +83,7 @@ public interface SubmissionService {
* @param attachmentId unique attachment identifier
* @return encrypted string of the attachment data
*/
String getAttachment(UUID submissionId, UUID attachmentId);
String getAttachment(UUID submissionId, UUID attachmentId) throws RestApiException;
/**
* Get the submissions {@link Destination} by id.
......@@ -93,5 +93,5 @@ public interface SubmissionService {
*
* @throws RestApiException if an error occurred
*/
Destination getDestination(UUID destinationID);
Destination getDestination(UUID destinationID) throws RestApiException;
}
package dev.fitko.fitconnect.api.services.validation;
import com.nimbusds.jose.jwk.KeyOperation;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import java.util.List;
import java.util.Map;
/**
......@@ -14,28 +21,14 @@ import java.util.Map;
public interface ValidationService {
/**
* Validates the public key consisting of the following steps:
* <p>
* <ul>
* <li>checks if the JSON Web Key has key operation `wrap_key`</li>
* <li>checks if the JSON Web Key is suitable for the encryption</li>
* <li>checks if the public key is matching the certificate referenced in the JWK</li>
* <li>checks the certificate chain up to the root certificate</li>
* <li>checks against a certificate revocation list and/or an OSCP-endpoint with signed response</li>
* </ul>
* @param publicKey the public JWK
* Validates the public key for integrity.
*
* @return {@link ValidationResult} that includes an error if the validation failed
*/
ValidationResult validateEncryptionPublicKey(RSAKey publicKey);
/**
* Validates the public signature key with key-operation `verify`
* @param publicKey the public JWK
* @param keyOperation key operation the public key be validated with, represents {@code key_ops} parameter in a JWK
*
* @param signatureKey the public signature JWK
* @return {@link ValidationResult} that includes an error if the validation failed
*/
ValidationResult validateSignaturePublicKey(RSAKey signatureKey);
ValidationResult validatePublicKey(final RSAKey publicKey, KeyOperation keyOperation);
/**
* Validates the metadata against a given schema.
......@@ -46,6 +39,38 @@ public interface ValidationService {
*/
ValidationResult validateMetadataSchema(Metadata metadata);
/**
* Validates the {@link Metadata} structure and contents to ensure its correctness.
*
* @param metadata the {@link Metadata} object that is validated
* @param submission the {@link Submission} of the validated metadata
* @param destination the {@link Destination} of the validated metadata
* @param authenticationTags the {@link AuthenticationTags} of the validated metadata
* @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema
*/
ValidationResult validateMetadata(Metadata metadata, Submission submission, Destination destination, AuthenticationTags authenticationTags);
/**
* Validates the attachment structure and contents to ensure its correctness.
*
* @param attachmentsForValidation list of attachments containing the hash, decrypted and encrypted data needed for validation
* @param authenticationTags the {@link AuthenticationTags} of the validated attachments
* @return a {@link ValidationResult}, contains an error if the attachment is invalid
*/
ValidationResult validateAttachments(List<AttachmentForValidation> attachmentsForValidation, final AuthenticationTags authenticationTags);
/**
* Validates the {@link Data} structure and contents to ensure its correctness.
*
* @param decryptedData the unencrypted data as byte[]
* @param submission the {@link Submission} of the validated data
* @param metadata the {@link Metadata} of the validated data
* @param authenticationTags the {@link AuthenticationTags} of the validated data
* @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema
*/
ValidationResult validateData(byte[] decryptedData, Submission submission, Metadata metadata, AuthenticationTags authenticationTags);
/**
* Validates a set event against a given schema.
*
......@@ -101,4 +126,5 @@ public interface ValidationService {
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
*/
ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret);
}
......@@ -30,7 +30,6 @@ public final class RoutingClient {
* @throws RoutingException if the signature validation failed
* @throws RestApiException if a technical error occurred during the query
*/
public List<Route> findDestinations(final DestinationSearch search) throws RoutingException, RestApiException {
final RouteResult routeResult = routingService.getRoutes(search.getLeikaKey(), search.getArs(), search.getAgs(), search.getAreaId(), search.getOffset(), search.getLimit());
final ValidationResult result = routeVerifier.validateRouteDestinations(routeResult.getRoutes(), search.getLeikaKey(), search.getArs());
......
package dev.fitko.fitconnect.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
......@@ -15,23 +13,17 @@ import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.DataIntegrityException;
import dev.fitko.fitconnect.api.exceptions.DecryptionException;
import dev.fitko.fitconnect.api.exceptions.EventCreationException;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException;
import dev.fitko.fitconnect.api.services.Subscriber;
import dev.fitko.fitconnect.client.sender.model.Attachment;
import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.client.subscriber.model.DecryptedAttachmentPayload;
import dev.fitko.fitconnect.client.subscriber.model.ReceivedData;
import dev.fitko.fitconnect.core.util.StopWatch;
import dev.fitko.fitconnect.client.subscriber.SubmissionReceiver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
......@@ -46,14 +38,15 @@ import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKN
public class SubscriberClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SubscriberClient.class);
private static final ObjectMapper MAPPER = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final int DEFAULT_SUBMISSION_LIMIT = 500;
private final Subscriber subscriber;
private final RSAKey privateKey;
private final SubmissionReceiver submissionReceiver;
public SubscriberClient(final Subscriber subscriber, final RSAKey privateKey) {
public SubscriberClient(final Subscriber subscriber, final SubmissionReceiver submissionReceiver) {
this.subscriber = subscriber;
this.privateKey = privateKey;
this.submissionReceiver = submissionReceiver;
}
/**
......@@ -103,59 +96,14 @@ public class SubscriberClient {
}
/**
* Loads a single {@link SubmissionForPickup} by id.
* Loads a single {@link SubmissionForPickup} by id. Auto-rejects invalid submissions where e.g. a validation failed.
*
* @param submissionId unique identifier of a submission
* @return {@link ReceivedSubmission} to get the submission {@link Metadata}, {@link Data} and {@link ApiAttachment}s as well as
* accept or reject the loaded submission
* @param submissionId unique identifier of the requested submission
* @return {@link ReceivedSubmission} to get the submission {@link Metadata}, {@link Data} and {@link Attachment}s as well as accept or reject the loaded submission
* @throws SubmissionRequestException if a technical error occurred or validation failed
*/
public ReceivedSubmission requestSubmission(final UUID submissionId) {
try {
final var startTimeDownloadSubmission = StopWatch.start();
final Submission submission = subscriber.getSubmission(submissionId);
LOGGER.info("Downloading submission took {}", StopWatch.stopWithFormattedTime(startTimeDownloadSubmission));
LOGGER.info("Decrypting metadata ...");
final Metadata metadata = decryptMetadata(submission.getEncryptedMetadata());
final ValidationResult metadataValidation = subscriber.validateMetadata(metadata);
if (metadataValidation.hasError()) {
LOGGER.error("Metadata does not match schema", metadataValidation.getError());
return null;
}
LOGGER.info("Decrypting data ...");
final byte[] decryptedData = decryptData(submission.getEncryptedData());
final String hashFromSender = getDataHashFromMetadata(metadata);
final ValidationResult dataValidation = subscriber.validateHashIntegrity(hashFromSender, decryptedData);
if (dataValidation.hasError()) {
LOGGER.error("Data might be corrupted, hash-sum does not match", dataValidation.getError());
return null;
}
LOGGER.info("Loading and decrypting attachments ...");
final List<ApiAttachment> attachmentMetadata = metadata.getContentStructure().getAttachments();
final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads = loadAttachments(submissionId, attachmentMetadata);
final ValidationResult attachmentValidation = validateAttachments(decryptedAttachmentPayloads);
if (attachmentValidation.hasError()) {
LOGGER.error("One of the attachments is invalid", attachmentValidation.getError());
return null;
}
return buildReceivedSubmission(submission, metadata, decryptedData, decryptedAttachmentPayloads);
} catch (final DecryptionException e) {
LOGGER.error("Decrypting payload failed", e);
} catch (final RestApiException e) {
LOGGER.error("API request failed", e);
} catch (final EventCreationException e) {
LOGGER.error("Creating SET event failed", e);
} catch (final IOException e) {
LOGGER.error("Reading metadata failed", e);
}
return null;
public ReceivedSubmission requestSubmission(final UUID submissionId) throws SubmissionRequestException {
return submissionReceiver.receiveSubmission(submissionId);
}
/**
......@@ -169,83 +117,16 @@ public class SubscriberClient {
subscriber.rejectSubmission(EventPayload.forRejectEvent(submissionForPickup, rejectionProblems));
}
/**
* Checks if a received callback can be trusted by validating the provided request data
*
* @param hmac authentication code provided by the callback
* @param timestamp timestamp provided by the callback
* @param httpBody HTTP body provided by the callback
* @param callbackSecret secret owned by the client, which is used to calculate the hmac
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
*/
public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
return subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<DecryptedAttachmentPayload> attachments) {
final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType();
final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType);
final List<Attachment> receivedAttachments = attachments.stream().map(this::mapToAttachment).collect(Collectors.toList());
// TODO setting auth tags was refactored in #598-auto-reject
return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments, Map.of());
}
private Attachment mapToAttachment(final DecryptedAttachmentPayload payload) {
final ApiAttachment metadata = payload.getAttachmentMetadata();
return Attachment.fromByteArray(payload.getDecryptedContent(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription());
}
private List<DecryptedAttachmentPayload> loadAttachments(final UUID submissionId, final List<ApiAttachment> attachmentMetadata) {
if (attachmentMetadata == null || attachmentMetadata.isEmpty()) {
LOGGER.info("Submission contains no attachments");
return Collections.emptyList();
}
final List<DecryptedAttachmentPayload> receivedAttachments = new ArrayList<>();
for (final ApiAttachment metadata : attachmentMetadata) {
final String encryptedAttachment = downloadAttachment(submissionId, metadata);
final byte[] decryptedAttachment = decryptAttachment(metadata, encryptedAttachment);
final DecryptedAttachmentPayload decryptedAttachmentPayload = DecryptedAttachmentPayload.builder()
.decryptedContent(decryptedAttachment)
.attachmentMetadata(metadata)
.build();
receivedAttachments.add(decryptedAttachmentPayload);
}
return receivedAttachments;
}
private byte[] decryptAttachment(final ApiAttachment metadata, final String encryptedAttachment) {
final var startDecryption = StopWatch.start();
final byte[] decryptedAttachment = subscriber.decryptStringContent(privateKey, encryptedAttachment);
LOGGER.info("Decrypting attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDecryption));
return decryptedAttachment;
}
private String downloadAttachment(final UUID submissionId, final ApiAttachment metadata) {
final var startDownload = StopWatch.start();
final String encryptedAttachment = subscriber.fetchAttachment(submissionId, metadata.getAttachmentId());
LOGGER.info("Downloading attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDownload));
return encryptedAttachment;
}
private byte[] decryptData(final String encryptedData) {
return subscriber.decryptStringContent(privateKey, encryptedData);
}
private Metadata decryptMetadata(final String encryptedMetadata) throws IOException {
final byte[] metadataBytes = subscriber.decryptStringContent(privateKey, encryptedMetadata);
return MAPPER.readValue(metadataBytes, Metadata.class);
}
private ValidationResult validateAttachments(final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads) {
for (final DecryptedAttachmentPayload decryptedAttachment : decryptedAttachmentPayloads) {
final ApiAttachment attachmentMetadata = decryptedAttachment.getAttachmentMetadata();
final UUID attachmentId = attachmentMetadata.getAttachmentId();
final ValidationResult result = subscriber.validateHashIntegrity(attachmentMetadata.getHash().getContent(), decryptedAttachment.getDecryptedContent());
if (result.hasError()) {
LOGGER.error("Attachment data for id {} is corrupted", attachmentId, result.getError());
return ValidationResult.error(new DataIntegrityException("Attachment " + attachmentId + " is corrupt"));
} else {
LOGGER.info("Attachment {} with id {} is valid", attachmentMetadata.getFilename(), attachmentId);
}
}
return ValidationResult.ok();
}
private String getDataHashFromMetadata(final Metadata metadata) {
return metadata.getContentStructure()
.getData()
.getHash()
.getContent();
}
}
......@@ -25,6 +25,7 @@ import dev.fitko.fitconnect.api.services.validation.ValidationService;
import dev.fitko.fitconnect.client.RoutingClient;
import dev.fitko.fitconnect.client.SenderClient;
import dev.fitko.fitconnect.client.SubscriberClient;
import dev.fitko.fitconnect.client.subscriber.SubmissionReceiver;
import dev.fitko.fitconnect.core.SubmissionSender;
import dev.fitko.fitconnect.core.SubmissionSubscriber;
import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
......@@ -96,7 +97,9 @@ public final class ClientFactory {
final String privateKeyPath = readPath(getPrivateDecryptionKeyPathFromSubscriber(subscriberConfig), "Decryption Key");
final RSAKey privateKey = readRSAKeyFromString(privateKeyPath);
return new SubscriberClient(subscriber, privateKey);
final SubmissionReceiver submissionReceiver = new SubmissionReceiver(subscriber, privateKey, config);
return new SubscriberClient(subscriber, submissionReceiver);
}
/**
* Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
......
......@@ -129,6 +129,15 @@ public class Attachment {
return new String(data, encoding);
}
/**
* Get the attachment content as string with a default UTF-8 encoding, e.g. in case the mime-type is json or xml.
*
* @return utf-8 encoded string of the attachments content.
*/
public String getDataAString() {
return new String(data, StandardCharsets.UTF_8);
}
/**
* Filename of the attachment. This filed is optional so it might be null.
* @return filename as string, null if not present
......
package dev.fitko.fitconnect.client.subscriber;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventPayload;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.event.problems.Problem;
import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.AttachmentEncryptionIssue;
import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.MissingAttachment;
import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue;
import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataEncryptionIssue;
import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataJsonSyntaxViolation;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.MissingAuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.NotExactlyOneSubmitEvent;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.AuthenticationTagsEmptyException;
import dev.fitko.fitconnect.api.exceptions.DecryptionException;
import dev.fitko.fitconnect.api.exceptions.EventLogException;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException;
import dev.fitko.fitconnect.api.exceptions.SubmitEventNotFoundException;
import dev.fitko.fitconnect.api.services.Subscriber;
import dev.fitko.fitconnect.client.sender.model.Attachment;
import dev.fitko.fitconnect.client.subscriber.model.ReceivedData;
import dev.fitko.fitconnect.core.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.nimbusds.jose.jwk.KeyOperation.UNWRAP_KEY;
public class SubmissionReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionReceiver.class);
private static final ObjectMapper MAPPER = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
private final Subscriber subscriber;
private final ApplicationConfig config;
private final RSAKey privateKey;
public SubmissionReceiver(final Subscriber subscriber, final RSAKey privateKey, final ApplicationConfig config) {
this.subscriber = subscriber;
this.privateKey = privateKey;
this.config = config;
}
public ReceivedSubmission receiveSubmission(final UUID submissionId) {
LOGGER.info("Requesting submission ...");
final Submission submission = loadSubmission(submissionId);
LOGGER.info("Loading authentication tags from event log ...");
final AuthenticationTags authenticationTags = loadAuthTagsForSubmitEvent(submission);
LOGGER.info("Decrypting metadata ...");
final Metadata metadata = decryptMetadata(submission);
validateMetadata(metadata, submission, authenticationTags);
LOGGER.info("Decrypting data ...");
final byte[] decryptedData = decryptData(submission);
validateData(submission, metadata, decryptedData, authenticationTags);
LOGGER.info("Loading and decrypting attachments ...");
final List<AttachmentForValidation> attachments = loadAttachments(submission, metadata);
validateAttachments(attachments, submission, authenticationTags);
return buildReceivedSubmission(submission, metadata, decryptedData, attachments);
}
private Submission loadSubmission(final UUID submissionId) {
try {
final var startTimeDownloadSubmission = StopWatch.start();
final Submission submission = subscriber.getSubmission(submissionId);
LOGGER.info("Downloading submission took {}", StopWatch.stopWithFormattedTime(startTimeDownloadSubmission));
return submission;
} catch (final RestApiException e) {
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private AuthenticationTags loadAuthTagsForSubmitEvent(final Submission submission) {
try {
return subscriber.getAuthenticationTagsForEvent(Event.SUBMIT, submission);
} catch (final EventLogException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#struktur--und-signaturpr%C3%BCfung-der-security-event-tokens
final InvalidEventLog problem = new InvalidEventLog();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail(), e);
} catch (final SubmitEventNotFoundException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#genau-ein-submit-event
final NotExactlyOneSubmitEvent problem = new NotExactlyOneSubmitEvent();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail());
} catch (final AuthenticationTagsEmptyException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tags-im-submit-event
final MissingAuthenticationTags problem = new MissingAuthenticationTags();
rejectSubmissionWithProblem(submission, problem);
throw new SubmissionRequestException(problem.getDetail());
}
}
private void validateMetadata(final Metadata metadata, final Submission submission, final AuthenticationTags authenticationTags) {
final ValidationResult validationResult = subscriber.validateMetadata(metadata, submission, authenticationTags);
evaluateValidationResult(submission, validationResult, "Metadata is invalid");
}
private void validateAttachments(final List<AttachmentForValidation> attachmentForValidation, final Submission submission, final AuthenticationTags authenticationTags) {
final ValidationResult validationResult = subscriber.validateAttachments(attachmentForValidation, authenticationTags);
evaluateValidationResult(submission, validationResult, "Attachment validation failed");
}
private void validateData(final Submission submission, final Metadata metadata, final byte[] decryptedData, final AuthenticationTags authenticationTags) {
final ValidationResult validationResult = subscriber.validateData(decryptedData, submission, metadata, authenticationTags);
evaluateValidationResult(submission, validationResult, "Data is invalid");
}
private void evaluateValidationResult(final Submission submission, final ValidationResult validationResult, final String errorMessage) throws SubmissionRequestException {
if (validationResult.hasProblems()) {
rejectSubmissionWithProblem(submission, validationResult.getProblems().toArray(new Problem[0]));
throw new SubmissionRequestException(validationResult.hasError() ? validationResult.getError().getMessage() : errorMessage);
} else if (validationResult.hasError()) {
LOGGER.error(validationResult.getError().getMessage(), validationResult.getError());
throw new SubmissionRequestException(validationResult.getError().getMessage(), validationResult.getError());
}
}
private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<AttachmentForValidation> attachments) {
final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType();
final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType);
final List<Attachment> receivedAttachments = attachments.stream().map(this::toAttachment).collect(Collectors.toList());
final Map<UUID, String> encryptedAttachments = attachments.stream().collect(Collectors.toMap(AttachmentForValidation::getAttachmentId, AttachmentForValidation::getEncryptedData));
return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments, encryptedAttachments);
}
private Attachment toAttachment(final AttachmentForValidation attachment) {
final ApiAttachment metadata = attachment.getAttachmentMetadata();
return Attachment.fromByteArray(attachment.getDecryptedData(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription());
}
private List<AttachmentForValidation> loadAttachments(final Submission submission, final Metadata metadata) {
final List<ApiAttachment> attachments = metadata.getContentStructure().getAttachments();
if (attachments == null || attachments.isEmpty()) {
LOGGER.info("Submission contains no attachments");
return Collections.emptyList();
}
final List<AttachmentForValidation> receivedAttachments = new ArrayList<>();
for (final ApiAttachment attachmentMetadata : attachments) {
final String encryptedAttachment = downloadAttachment(submission, attachmentMetadata);
final byte[] decryptedAttachment = decryptAttachment(attachmentMetadata, encryptedAttachment, submission);
receivedAttachments.add(new AttachmentForValidation(attachmentMetadata, encryptedAttachment, decryptedAttachment));
}
return receivedAttachments;
}
private byte[] decryptAttachment(final ApiAttachment metadata, final String encryptedAttachment, final Submission submission) {
checkPrivateKey();
try {
final var startDecryption = StopWatch.start();
final byte[] decryptedAttachment = subscriber.decryptStringContent(privateKey, encryptedAttachment);
LOGGER.info("Decrypting attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDecryption));
return decryptedAttachment;
} catch (final DecryptionException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung-2
rejectSubmissionWithProblem(submission, new AttachmentEncryptionIssue(metadata.getAttachmentId()));
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private String downloadAttachment(final Submission submission, final ApiAttachment metadata) {
try {
final var startDownload = StopWatch.start();
final String encryptedAttachment = subscriber.fetchAttachment(submission.getSubmissionId(), metadata.getAttachmentId());
LOGGER.info("Downloading attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDownload));
return encryptedAttachment;
} catch (final RestApiException e) {
if (e.getHttpStatus() != null && e.getHttpStatus().equals(HttpStatus.NOT_FOUND)) {
rejectSubmissionWithProblem(submission, new MissingAttachment(metadata.getAttachmentId()));
}
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private byte[] decryptData(final Submission submission) {
checkPrivateKey();
try {
return subscriber.decryptStringContent(privateKey, submission.getEncryptedData());
} catch (final DecryptionException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung-1
rejectSubmissionWithProblem(submission, new DataEncryptionIssue());
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private Metadata decryptMetadata(final Submission submission) {
checkPrivateKey();
try {
final byte[] metadataBytes = subscriber.decryptStringContent(privateKey, submission.getEncryptedMetadata());
return MAPPER.readValue(metadataBytes, Metadata.class);
} catch (final IOException e) {
rejectSubmissionWithProblem(submission, new MetadataJsonSyntaxViolation());
throw new SubmissionRequestException(e.getMessage(), e);
} catch (final DecryptionException e) {
// https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung
rejectSubmissionWithProblem(submission, new MetadataEncryptionIssue());
throw new SubmissionRequestException(e.getMessage(), e);
}
}
private void checkPrivateKey() {
if (!isUnwrapKey(privateKey) && !isEncryptionKeyUse(privateKey)) {
throw new SubmissionRequestException("Private key is not suitable for decryption, could not find key_operation unwrapKey or key_use encryption");
}
}
private static boolean isEncryptionKeyUse(final RSAKey privateKey) {
return privateKey.getKeyUse() != null && privateKey.getKeyUse().equals(KeyUse.ENCRYPTION);
}
private static boolean isUnwrapKey(final RSAKey privateKey) {
return privateKey.getKeyOperations() != null && privateKey.getKeyOperations().stream()
.anyMatch(keyOp -> keyOp.identifier().equals(UNWRAP_KEY.identifier()));
}
private void rejectSubmissionWithProblem(final Submission submission, final Problem... problem) {
reject(submission, List.of(problem));
}
private void reject(final Submission submission, final List<Problem> problems) {
if (config.isEnableAutoReject()) {
subscriber.rejectSubmission(EventPayload.forRejectEvent(submission, problems));
}
}
}
......@@ -102,7 +102,6 @@ public class SenderClientTest {
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.createSubmission(any())).thenReturn(announcedSubmission);
when(senderMock.sendSubmission(any())).thenReturn(expectedSubmission);
when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok());
when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey);
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok());
......@@ -144,7 +143,6 @@ public class SenderClientTest {
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.createSubmission(any())).thenReturn(announcedSubmission);
when(senderMock.sendSubmission(any())).thenReturn(expectedSubmission);
when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok());
when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey);
when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok());
when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok());
......@@ -184,7 +182,6 @@ public class SenderClientTest {
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.createSubmission(any())).thenReturn(announcedSubmission);
when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok());
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey);
......@@ -379,7 +376,6 @@ public class SenderClientTest {
final var destination = getDestination(destinationId);
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok());
when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey);
// When
......@@ -509,7 +505,6 @@ public class SenderClientTest {
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.createSubmission(any())).thenReturn(announcedSubmission);
when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok());
when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey);
when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok());
when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok());
......
{"alg":"RSA-OAEP-256","d":"UuZ7DtiI46pxCeACoP6cDxWBT2PJea6Td5eTU2sm-jkGH1O_ShgBtqHI6KwOLenO0Z1GlHoVNzIx9NeazUpVdN1Oan42ximA6LXqP6GaaWD7qJmzuSER968F4StW7hv5aI1N35mFLESY2ENISfhmRGBsCalRrQVFU5lRwjH22m_JTdeMeRbCCZHxynH2GcOwei8lZgWTiu3cIz7giEwc3GrZHZMxhdBLBCJ3gxDer9O_V0UCr7W7VpLpFuaOZmKaB2io-Rgz5W71IIo-09IghWnV5VZWpXr1Dl1_UbhE9UK_N6gQ9HcFopBQR6huD2svUO2aVig3RLO1KG8AL6vQsp8uIa_7_TuVYnuo7S_zVRlungjs2UzABjRqsdUED9C-FmwpnbKcHA1V99U8Ag22VHXm0zcT58M09-_yUl9s4VG6y-yQEhfxTjL0LMtSL8XScRwf38mpgX54LFGOeImo8d6jFGbNMzMViStejmXU8nCBeRmEtHpDU-GxJSPhO0ZJNgNWH_RU-tmujodXxXvyp7yA3MFqvcKxUQGf73lz_y10lLSmAJSlG-3KYyHY1N5AlXF4m0N3yENcim3VPVZarpOgHfEF3gg3slthAFZqTeY2WKMBp2O_QBvyf3mylVKHECzY4XzScBiPA9qlbeRDoa-NxcQwT2bS_1cyk8_IYG8","dp":"-sRGGYORpYDRglqvK9RX6FCQUpjCwbyAeq3yU4sEnC2XWt37-bw-p0KzSs7OuTTSPNH5I9HrfAzVHPzGXWE_CJmkdvkGqLkmvFTFiuGY1yk3psYvLwzDPJjyKUzdI-GMdioL_IyjEfna2TxdiYLyzGCDtGJxrSVyunc2xcMx_k1u_HQcylog00P61kCnUVtGsDqMbYsShCQaZXwQK3wPL6KkjgVLtEYYYeP8NiJRL_XP8SCD4ebU_cJMLDWgq1DgxpfqIBz0fCNe0ty9LwEhMhmP6OpmU5mRAGzXNKOp_lYYEVmvRoEwcRyFdM1W4N61eu8EYGyfzTrH1gfG8FMLfQ","dq":"uC9tAhQI7vRE2isCeoxY3YgJd2RBOdRrIjowUoS7WuEho_aLKWgoJDSzZoXad9pindjaSRj9CRKZ7oRW1Dp0NQwTQdUsyZSZt4cB5yxg50ty5IE81XDX0pgyRCJOXNpKxSZYtcBUzqlId4dCRd2codqaKJkP77K0TmWlQve_CoCPuaSP4CGqt8GpQj1fKzI_NYYqNVk-1jSAa9PZ2E-gW0iJwQoUNkDRTBgIVLx1qu7aYieYeif8oTFjKGKkP3EXCTGXKSdLd729-37NgWR0YktLFQsWfiNHcy1_-eWI6HKc8onOv5IuchKQ9nmQIE1mQUZBATCtx-vdzQqF_alTKw","e":"AQAB","key_ops":["unwrapKey"],"kid":"jsMQHFiA2uMGtiG3Ro8RJnYdEhp5W5KjGW-Vcf3-YMk","kty":"RSA","n":"zEi7NpGkzGyG2-PDWTy72hvti-pGBZLidxbk10_fenjzOavKcO5yGXSCfX_Xl0-WYaJfb7Kz6CRRtnwBGx8mrsftodtxt3kdrFnf6chQ2JJ5dmnz_ErIbHjHaFlxXvEqv3ivqIQSZvuns8QJip8RGQ63g7nmPDyvBcvLMFUtnXy7Y6rzUI4HO2eeg8htMPCWN5-L7Ol5_IT11NuZK6J3_UN1B0P7fFGdVMhsNR5D_jHOD-U4jZQijyVgzIXxwN_vnf90e5_ZUgsLypwh2DT7qzhqES8hzIIk_Cjs5mCUwDjfiBh0g7LNjXaj0b7rAn2X6yPuMK6zYi_FXJYSCQ7LH3THi_h5r2--xQ2h40He23JjfNpEGu98uMgB_dvzH0Dco8OKh4Aj2wjLpZVFOS3Zpu-WUalP-ecEiZ6nzmCMoIpWM0U4t4tpGwC1X-MWNj75zYLI8-yd3ELVBx3PQSzDukGOVLBdtpF0txf7Np0i9RFRW-nxf3xPVFCbZYbc87GMZy1CeDT2NBJJHtQaPKBismnEXtXtiM7aJDRM74F5JXjDVR62eHGxFCy46oyI_NS3FmvkoVtV8hvbXBRMSU8sNeOrVKnbWpfjBGFGD8VbssKoAAdMfn6usOiiH71SBg-L9KoBwdRfu7XFE-WbSA2d03hMvEPJWk9srh5qGdtQ_QE","p":"_RRumCEIUoM1gipAOIeQZ_td0E2qx20bFHArBf1V7ev4v4S22tt1Bt9oa20KPfA6mokycaB8MBCX7LTRvBT1XwSh4sU2TptuibElrarQvFoJIVGqugqG4GxsBpZpy-Dvx9NJtMTjiiGXSn87-_8iSWrPlc4Vdbgb8S9Vz2x_Mp-uSwZaFOO3Gx4miD29y1ugE26hFF0RdgBDJnJEk71W9NG2D_MbnBBa3DrPesdU8rX08RurpV8C1TVUgelDxUBQUxrRXRaHOPbrwPBBRDOgzNlFiuzUdkw6qwlfhp9W1tn3j7uAnqMHxnaPhJJ51tFjd_7MgSawmmM415c4NWa5xw","q":"zqQpmsvtjeU486C2DZ_NjsDHanYXgfxiY3X25XpM0ojLrtIQWKUaFxlggJGSQzQ6QJqB-5sJu9EEE4DGZ3UYoakY_hzxv0uHOHrI20ohFgF5K3YQdqGRhC3Bo66ezpmD3FT1EPXQM2OLEAi82lUb7UrbP_DSU0BmYOd6kPd1a3FD2qm0DjA15IYW5vUHNisx79W3kt_8s7ZICjiVpvJTSa1TVIX3wPM0qLHOWbHEOswLENC4fZ7EFYkNr3MvtAOxLcJ_mi4zzhAkyGdKJbTQXt6bhD_gWL5K1iOFM5Vg_NRTjUMqndDK33KDYZhC-MexSR-IaN7MbP1Y787uFL5S9w","qi":"DHzvGmAC4OZIfvDBkqXrvrvBeqURfioiEN72SL6oPcBqof899UL50BVohU0a2IeydwLSSff1i0U0jfX8ss_YKnCw2MxH3eNdQ-JXAQp7sefYLiFKPlGQnaxCNL0Z3kDatPhJnfTHaJA_UKtuPtcIINK5lJH80Iqqg3rl1p5G84ujbIghcgJXGoRGjJfkpUQgarTwPzdScR8Xo-YmN4dNav2X76s5toTJbSc4wLh3z_vdgL4GTGv9jzRKBPJcv07SDOFz92VA2r2DiIPQxg1eAKq8MPwEQ_bkaqh6Sk57_-JT8CRN0vfZbHw_3jCc8ijqRQE-4HaKcVCAbgnTzgr8cw"}
\ No newline at end of file
{
"alg": "RSA-OAEP-256",
"d": "UuZ7DtiI46pxCeACoP6cDxWBT2PJea6Td5eTU2sm-jkGH1O_ShgBtqHI6KwOLenO0Z1GlHoVNzIx9NeazUpVdN1Oan42ximA6LXqP6GaaWD7qJmzuSER968F4StW7hv5aI1N35mFLESY2ENISfhmRGBsCalRrQVFU5lRwjH22m_JTdeMeRbCCZHxynH2GcOwei8lZgWTiu3cIz7giEwc3GrZHZMxhdBLBCJ3gxDer9O_V0UCr7W7VpLpFuaOZmKaB2io-Rgz5W71IIo-09IghWnV5VZWpXr1Dl1_UbhE9UK_N6gQ9HcFopBQR6huD2svUO2aVig3RLO1KG8AL6vQsp8uIa_7_TuVYnuo7S_zVRlungjs2UzABjRqsdUED9C-FmwpnbKcHA1V99U8Ag22VHXm0zcT58M09-_yUl9s4VG6y-yQEhfxTjL0LMtSL8XScRwf38mpgX54LFGOeImo8d6jFGbNMzMViStejmXU8nCBeRmEtHpDU-GxJSPhO0ZJNgNWH_RU-tmujodXxXvyp7yA3MFqvcKxUQGf73lz_y10lLSmAJSlG-3KYyHY1N5AlXF4m0N3yENcim3VPVZarpOgHfEF3gg3slthAFZqTeY2WKMBp2O_QBvyf3mylVKHECzY4XzScBiPA9qlbeRDoa-NxcQwT2bS_1cyk8_IYG8",
"dp": "-sRGGYORpYDRglqvK9RX6FCQUpjCwbyAeq3yU4sEnC2XWt37-bw-p0KzSs7OuTTSPNH5I9HrfAzVHPzGXWE_CJmkdvkGqLkmvFTFiuGY1yk3psYvLwzDPJjyKUzdI-GMdioL_IyjEfna2TxdiYLyzGCDtGJxrSVyunc2xcMx_k1u_HQcylog00P61kCnUVtGsDqMbYsShCQaZXwQK3wPL6KkjgVLtEYYYeP8NiJRL_XP8SCD4ebU_cJMLDWgq1DgxpfqIBz0fCNe0ty9LwEhMhmP6OpmU5mRAGzXNKOp_lYYEVmvRoEwcRyFdM1W4N61eu8EYGyfzTrH1gfG8FMLfQ",
"dq": "uC9tAhQI7vRE2isCeoxY3YgJd2RBOdRrIjowUoS7WuEho_aLKWgoJDSzZoXad9pindjaSRj9CRKZ7oRW1Dp0NQwTQdUsyZSZt4cB5yxg50ty5IE81XDX0pgyRCJOXNpKxSZYtcBUzqlId4dCRd2codqaKJkP77K0TmWlQve_CoCPuaSP4CGqt8GpQj1fKzI_NYYqNVk-1jSAa9PZ2E-gW0iJwQoUNkDRTBgIVLx1qu7aYieYeif8oTFjKGKkP3EXCTGXKSdLd729-37NgWR0YktLFQsWfiNHcy1_-eWI6HKc8onOv5IuchKQ9nmQIE1mQUZBATCtx-vdzQqF_alTKw",
"e": "AQAB",
"key_ops": [
"unwrapKey"
],
"kid": "jsMQHFiA2uMGtiG3Ro8RJnYdEhp5W5KjGW-Vcf3-YMk",
"kty": "RSA",
"n": "zEi7NpGkzGyG2-PDWTy72hvti-pGBZLidxbk10_fenjzOavKcO5yGXSCfX_Xl0-WYaJfb7Kz6CRRtnwBGx8mrsftodtxt3kdrFnf6chQ2JJ5dmnz_ErIbHjHaFlxXvEqv3ivqIQSZvuns8QJip8RGQ63g7nmPDyvBcvLMFUtnXy7Y6rzUI4HO2eeg8htMPCWN5-L7Ol5_IT11NuZK6J3_UN1B0P7fFGdVMhsNR5D_jHOD-U4jZQijyVgzIXxwN_vnf90e5_ZUgsLypwh2DT7qzhqES8hzIIk_Cjs5mCUwDjfiBh0g7LNjXaj0b7rAn2X6yPuMK6zYi_FXJYSCQ7LH3THi_h5r2--xQ2h40He23JjfNpEGu98uMgB_dvzH0Dco8OKh4Aj2wjLpZVFOS3Zpu-WUalP-ecEiZ6nzmCMoIpWM0U4t4tpGwC1X-MWNj75zYLI8-yd3ELVBx3PQSzDukGOVLBdtpF0txf7Np0i9RFRW-nxf3xPVFCbZYbc87GMZy1CeDT2NBJJHtQaPKBismnEXtXtiM7aJDRM74F5JXjDVR62eHGxFCy46oyI_NS3FmvkoVtV8hvbXBRMSU8sNeOrVKnbWpfjBGFGD8VbssKoAAdMfn6usOiiH71SBg-L9KoBwdRfu7XFE-WbSA2d03hMvEPJWk9srh5qGdtQ_QE",
"p": "_RRumCEIUoM1gipAOIeQZ_td0E2qx20bFHArBf1V7ev4v4S22tt1Bt9oa20KPfA6mokycaB8MBCX7LTRvBT1XwSh4sU2TptuibElrarQvFoJIVGqugqG4GxsBpZpy-Dvx9NJtMTjiiGXSn87-_8iSWrPlc4Vdbgb8S9Vz2x_Mp-uSwZaFOO3Gx4miD29y1ugE26hFF0RdgBDJnJEk71W9NG2D_MbnBBa3DrPesdU8rX08RurpV8C1TVUgelDxUBQUxrRXRaHOPbrwPBBRDOgzNlFiuzUdkw6qwlfhp9W1tn3j7uAnqMHxnaPhJJ51tFjd_7MgSawmmM415c4NWa5xw",
"q": "zqQpmsvtjeU486C2DZ_NjsDHanYXgfxiY3X25XpM0ojLrtIQWKUaFxlggJGSQzQ6QJqB-5sJu9EEE4DGZ3UYoakY_hzxv0uHOHrI20ohFgF5K3YQdqGRhC3Bo66ezpmD3FT1EPXQM2OLEAi82lUb7UrbP_DSU0BmYOd6kPd1a3FD2qm0DjA15IYW5vUHNisx79W3kt_8s7ZICjiVpvJTSa1TVIX3wPM0qLHOWbHEOswLENC4fZ7EFYkNr3MvtAOxLcJ_mi4zzhAkyGdKJbTQXt6bhD_gWL5K1iOFM5Vg_NRTjUMqndDK33KDYZhC-MexSR-IaN7MbP1Y787uFL5S9w",
"qi": "DHzvGmAC4OZIfvDBkqXrvrvBeqURfioiEN72SL6oPcBqof899UL50BVohU0a2IeydwLSSff1i0U0jfX8ss_YKnCw2MxH3eNdQ-JXAQp7sefYLiFKPlGQnaxCNL0Z3kDatPhJnfTHaJA_UKtuPtcIINK5lJH80Iqqg3rl1p5G84ujbIghcgJXGoRGjJfkpUQgarTwPzdScR8Xo-YmN4dNav2X76s5toTJbSc4wLh3z_vdgL4GTGv9jzRKBPJcv07SDOFz92VA2r2DiIPQxg1eAKq8MPwEQ_bkaqh6Sk57_-JT8CRN0vfZbHw_3jCc8ijqRQE-4HaKcVCAbgnTzgr8cw"
}
\ No newline at end of file