diff --git a/cli/README.md b/cli/README.md
index 9d9ca485e698761807456e1560309695298408cf..7c506fe50f1cb9931fde59eb285dfa8e75fa8ef1 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -5,24 +5,21 @@ The sdk provides a commandline-client as a runnable .jar, to be able to use the
 
 #### Setup & Build
 1. Build project root wih ``./mvnw clean package``
-2. Go to client/target and find a runnable jar ``fit-connect-client.jar``
+2. Go to client/target and find a runnable jar ``fit-connect-cli.jar``
 3. Provide [config yaml](../config.yml):
-   1. set environment variable ``FIT_CONNECT_CONFIG``:
-      1. Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml``
-      2. Windows: ``set FIT_CONNECT_CONFIG=C:\Path\To\config.yml``
-   2. Initialize client via ApplicationConfigLoader:
-      ````java
-      var config = ApplicationConfigLoader.loadConfig("absolute/path/to/config.yml");
-      var senderClient = ClientFactory.senderClient(config);
-      ````
-   3. put ``config.yml`` in same path as the .jar-file 
-5. run client with ``java -jarfit-connect-client.jar [COMMAND] [OPTIONS]``
+   * a) set environment variable **FIT_CONNECT_CONFIG** or
+   * b) put ``config.yml`` in same path as the .jar-file 
+4. run client with ``java -jarfit-connect-cli.jar [COMMAND] [OPTIONS]``
+
+> **Setting Env-Variables:** <br>
+ Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml`` <br>
+ Windows: ``set FIT_CONNECT_CONFIG=C:\Path\To\config.yml``
 
 #### SEND Single Submission Example
 The send command submits a new submission to a destination. Apart from optional attachments, all options are mandatory.
 
 ````sh
-java -jar fit-connect-client.jar send 
+java -jar fit-connect-cli.jar send 
     --destinationId=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
     --serviceName=Test 
     --leikaKey=urn:de:fim:leika:leistung:99400048079000 
@@ -35,7 +32,7 @@ java -jar fit-connect-client.jar send
 #### LIST Submissions Example
 The list command lists all submissionIds for a given destinationID.
 ````sh
-java -jar fit-connect-client.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
+java -jar fit-connect-cli.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
 ````
 #### GET Single Submission Example
 The get command loads a submission by `submissionId` and stores data and attachments in the given target location. If no target is
@@ -43,13 +40,13 @@ set, the cmd-client saves the data into in a folder named by the `submissionId`
 jar.
 
 ````sh
-java -jar fit-connect-client.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
+java -jar fit-connect-cli.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
 ````
 #### Batch Mode
 To send multiple submission with a single command, the client can be used in batch mode:
 
 ````sh
-java -jar fit-connect-client.jar batch --data=batch_data.csv
+java -jar fit-connect-cli.jar batch --data=batch_data.csv
 ````
 
 Currently, the import of CSV is supported. Follow the schema below, setting up your data:
@@ -60,11 +57,26 @@ destinationId, serviceName, leikaKey, data, dataType, attachments
 1b7d1a24-a6c8-4050-bb71-ae3749ec432d, Test2, "urn:de:fim:leika:leistung:99400048079000", /path/to/data/data2.xml, "XML", "path/to/attachment/report.pdf"
 ````
 > Windows paths: escape windows paths with `\\`, e.g. `C:\\path\\to\\file\\file.txt` !
-> 
+
+#### Generate JWK Test Keys
+The CLI can generate public and private keys for testing purposes for both encryption and signing.
+If no ``outDir`` ist specified, the cli will create a temporary folder that is logged in the console output.
+
+````sh
+java -jar fit-connect-cli.jar keygen --outDir=C:\temp
+````
+```
+[main] INFO  d.f.fitconnect.cli.CommandExecutor Generating JWKs ...
+[main] INFO  d.f.fitconnect.cli.keygen.KeyWriter Writing keys to directory C:\temp
+[main] INFO  d.f.fitconnect.cli.keygen.KeyWriter Wrote Encryption Public Key (key_use=wrapKey) as publicKey_encryption.json
+[main] INFO  d.f.fitconnect.cli.keygen.KeyWriter Wrote Decryption Private Key (key_use=unwrapKey) as privateKey_decryption.json
+[main] INFO  d.f.fitconnect.cli.keygen.KeyWriter Wrote Signature Verification Public Key (key_use=verify) as publicKey_signature_verification.json
+[main] INFO  d.f.fitconnect.cli.keygen.KeyWriter Wrote Signing Private Key (key_use=sign) as privateKey_signing.json
+```
+
 #### Usage And Commands
 
 ````shell
-Usage: <main class> [command] [command options]
   Commands:
     send      Send a submission
       Usage: send [options]
@@ -81,6 +93,8 @@ Usage: <main class> [command] [command options]
             Unique destination identifier in UUID format
         * --leikaKey
             The LeikaKey of the service type
+        * --schemaUri
+            Schema URI to validate submission data
         * --serviceName
             Name of the service type
 
@@ -111,4 +125,15 @@ Usage: <main class> [command] [command options]
         Options:
         * --data
             Path to submission data as csv
+
+    keygen      Generates JWK test keys for encryption, decryption, signing
+            and signature validation
+      Usage: keygen [options]
+        Options:
+          --outDir
+            Output directory folder where the generated test keys are written
+            to
+          --withConfig
+            Generates config.yaml with paths of the generated keys
+            Default: false
 ````
diff --git a/cli/pom.xml b/cli/pom.xml
index 8d9e7e07d453bf4593fae15af13df69f33e0aa09..9d26020eec770c73e756671dfa1afeb87d60be04 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -11,7 +11,7 @@
 
     <artifactId>cli</artifactId>
     <packaging>jar</packaging>
-    <name>FIT-Connect Java SDK - Command-Line Interface</name>
+    <name>FIT-Connect Java SDK - CLI</name>
 
     <dependencies>
         <dependency>
@@ -75,7 +75,7 @@
                     <descriptorRefs>
                         <descriptorRef>jar-with-dependencies</descriptorRef>
                     </descriptorRefs>
-                    <finalName>fit-connect-client</finalName>
+                    <finalName>fit-connect-cli</finalName>
                     <appendAssemblyId>false</appendAssemblyId>
                 </configuration>
                 <executions>
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandExecutor.java b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandExecutor.java
index e9b3beae875cf52e967384e5b41159e935a7f190..26beed4e404d15ad4c99752f5c58bee533215e83 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandExecutor.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandExecutor.java
@@ -3,16 +3,21 @@ package dev.fitko.fitconnect.cli;
 import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
 import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
 import dev.fitko.fitconnect.api.exceptions.internal.BatchImportException;
+import dev.fitko.fitconnect.cli.batch.BatchImporter;
 import dev.fitko.fitconnect.cli.batch.ImportRecord;
+import dev.fitko.fitconnect.cli.commands.CreateTestKeysCommand;
 import dev.fitko.fitconnect.cli.commands.GetAllSubmissionsCommand;
 import dev.fitko.fitconnect.cli.commands.GetOneSubmissionCommand;
 import dev.fitko.fitconnect.cli.commands.ListAllSubmissionsCommand;
 import dev.fitko.fitconnect.cli.commands.SendBatchCommand;
 import dev.fitko.fitconnect.cli.commands.SendSubmissionCommand;
+import dev.fitko.fitconnect.cli.keygen.JWKGenerator;
+import dev.fitko.fitconnect.cli.keygen.JWKPair;
+import dev.fitko.fitconnect.cli.keygen.KeyWriter;
+import dev.fitko.fitconnect.cli.keygen.KeyWriterSettings;
+import dev.fitko.fitconnect.cli.util.AttachmentDataType;
 import dev.fitko.fitconnect.client.SenderClient;
 import dev.fitko.fitconnect.client.SubscriberClient;
-import dev.fitko.fitconnect.cli.batch.BatchImporter;
-import dev.fitko.fitconnect.cli.util.AttachmentDataType;
 import dev.fitko.fitconnect.client.sender.model.Attachment;
 import dev.fitko.fitconnect.client.sender.model.SendableSubmission;
 import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
@@ -29,42 +34,47 @@ import java.nio.file.Path;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static dev.fitko.fitconnect.cli.keygen.JWKGenerator.DEFAULT_KEY_SIZE;
+
 class CommandExecutor {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class);
 
     private static final String IMPORTED_RECORD_TEMPLATE = "\n\n================== Imported record {} ==================\n";
 
-    private final SenderClient senderClient;
-    private final SubscriberClient subscriberClient;
     private final BatchImporter batchImporter;
+    private final JWKGenerator jwkGenerator;
+    private final Supplier<SenderClient> senderSupplier;
+    private final Supplier<SubscriberClient> subscriberSupplier;
 
-    CommandExecutor(final SenderClient senderClient, final SubscriberClient subscriberClient, final BatchImporter batchImporter) {
-        this.senderClient = senderClient;
-        this.subscriberClient = subscriberClient;
+    CommandExecutor(final Supplier<SenderClient> senderSupplier, final Supplier<SubscriberClient> subscriberSupplier, final BatchImporter batchImporter, final JWKGenerator jwkGenerator) {
         this.batchImporter = batchImporter;
+        this.jwkGenerator = jwkGenerator;
+        this.senderSupplier = senderSupplier;
+        this.subscriberSupplier = subscriberSupplier;
     }
 
     void getOneSubmission(final GetOneSubmissionCommand getOneSubmissionCommand) throws IOException {
         LOGGER.info("Getting submission for id {}", getOneSubmissionCommand.submissionId);
         final var startTime = StopWatch.start();
-        final var submission = subscriberClient.requestSubmission(getOneSubmissionCommand.submissionId);
+        final var submission = subscriberSupplier.get().requestSubmission(getOneSubmissionCommand.submissionId);
         LOGGER.info("Submission download took {}", StopWatch.stopWithFormattedTime(startTime));
         if (submission == null) {
             LOGGER.info("No submission found for submission id {}", getOneSubmissionCommand.submissionId);
         } else {
             submission.acceptSubmission();
-            writeData(submission, getTargetFolderPath(getOneSubmissionCommand));
+            writeSubmissionData(submission, getTargetFolderPath(getOneSubmissionCommand));
         }
     }
 
     void getAllSubmissions(final GetAllSubmissionsCommand getAllSubmissionsCommand) throws IOException {
         final var destinationId = getAllSubmissionsCommand.destinationId;
         LOGGER.info("Getting all available submissions for destination {}", destinationId);
-        final Set<SubmissionForPickup> submissions = subscriberClient.getAvailableSubmissionsForDestination(destinationId);
+        final Set<SubmissionForPickup> submissions = subscriberSupplier.get().getAvailableSubmissionsForDestination(destinationId);
         for (final SubmissionForPickup submission : submissions) {
             final GetOneSubmissionCommand getOneSubmissionCommand = new GetOneSubmissionCommand();
             getOneSubmissionCommand.submissionId = submission.getSubmissionId();
@@ -76,7 +86,7 @@ class CommandExecutor {
     void listSubmissions(final ListAllSubmissionsCommand listAllSubmissionsCommand) {
         final var destinationId = listAllSubmissionsCommand.destinationId;
         LOGGER.info("Listing available submissions for destination {}", destinationId);
-        for (final SubmissionForPickup submission : subscriberClient.getAvailableSubmissionsForDestination(destinationId)) {
+        for (final SubmissionForPickup submission : subscriberSupplier.get().getAvailableSubmissionsForDestination(destinationId)) {
             LOGGER.info("caseId: {} - submissionId: {}", submission.getCaseId(), submission.getSubmissionId());
         }
     }
@@ -95,6 +105,7 @@ class CommandExecutor {
         LOGGER.info("Import took {}", StopWatch.stopWithFormattedTime(startTime));
         return submission;
     }
+
     void sendBatch(final SendBatchCommand sendBatchCommand) throws BatchImportException, IOException {
         final List<ImportRecord> importRecords = batchImporter.readRecords(sendBatchCommand.dataPath);
         LOGGER.info("Sending batch of {} submissions", importRecords.size());
@@ -111,6 +122,23 @@ class CommandExecutor {
         LOGGER.info("DONE ! Finished batch import of {} submissions in {}", importCount, StopWatch.stopWithFormattedTime(startTime));
     }
 
+    void createTestKeys(final CreateTestKeysCommand createTestKeysCommand) {
+
+        LOGGER.info("Generating JWKs ...");
+
+        final JWKPair encryptionKeyPair = jwkGenerator.generateEncryptionKeyPair(DEFAULT_KEY_SIZE);
+        final JWKPair signatureKeyPair = jwkGenerator.generateSignatureKeyPair(DEFAULT_KEY_SIZE);
+
+        final KeyWriterSettings keyWriterSettings = KeyWriterSettings.builder()
+                .outputDir(createTestKeysCommand.outputDir)
+                .createConfigYaml(createTestKeysCommand.generateConfig)
+                .encryptionKeyPair(encryptionKeyPair)
+                .signatureKeyPair(signatureKeyPair)
+                .build();
+
+        new KeyWriter().writeKeys(keyWriterSettings);
+    }
+
     private SentSubmission sendWithJsonData(final SendSubmissionCommand sendSubmissionCommand, final List<Attachment> attachments) throws IOException {
 
         final SendableSubmission sendableSubmission = SendableSubmission.Builder()
@@ -120,7 +148,7 @@ class CommandExecutor {
                 .addAttachments(attachments)
                 .build();
 
-        return senderClient.send(sendableSubmission);
+        return senderSupplier.get().send(sendableSubmission);
     }
 
     private SentSubmission sendWithXmlData(final SendSubmissionCommand sendSubmissionCommand, final List<Attachment> attachments) throws IOException {
@@ -132,7 +160,7 @@ class CommandExecutor {
                 .addAttachments(attachments)
                 .build();
 
-        return senderClient.send(sendableSubmission);
+        return senderSupplier.get().send(sendableSubmission);
     }
 
     private String getTargetFolderPath(final GetOneSubmissionCommand getOneSubmissionCommand) {
@@ -152,7 +180,7 @@ class CommandExecutor {
         return path -> Attachment.fromPath(Path.of(path), mimeTypeDetector.detect(path), Path.of(path).getFileName().toString(), "attachment");
     }
 
-    private void writeData(final ReceivedSubmission receivedSubmission, final String dataDirPath) throws IOException {
+    private void writeSubmissionData(final ReceivedSubmission receivedSubmission, final String dataDirPath) throws IOException {
         LOGGER.info("Creating data directory for submission in {}", dataDirPath);
         Files.createDirectories(Path.of(dataDirPath));
 
@@ -161,8 +189,10 @@ class CommandExecutor {
         LOGGER.info("Writing data.{}", fileEnding);
         Files.write(filePath, receivedSubmission.getDataAsString().getBytes(StandardCharsets.UTF_8));
 
-        for (final Attachment attachment : receivedSubmission.getAttachments()) {
-            final String filename = attachment.getFileName();
+        final List<Attachment> attachments = receivedSubmission.getAttachments();
+        for (int i = 0; i < attachments.size(); i++) {
+            final Attachment attachment = attachments.get(i);
+            final String filename = attachment.getFileName() != null ? attachment.getFileName() : "attachment_" + i;
             LOGGER.info("Writing attachment {}", filename);
             Files.write(Path.of(dataDirPath, filename), attachment.getDataAsBytes());
         }
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineClient.java b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineClient.java
index 776e9ef3415546c9b6dafd8e9574fe8562c04608..1186083cae8f19981b882909ad8b664e213657ab 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineClient.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineClient.java
@@ -2,18 +2,21 @@ package dev.fitko.fitconnect.cli;
 
 import com.beust.jcommander.JCommander;
 import com.beust.jcommander.ParameterException;
-import dev.fitko.fitconnect.client.SenderClient;
-import dev.fitko.fitconnect.client.SubscriberClient;
 import dev.fitko.fitconnect.cli.batch.CsvImporter;
+import dev.fitko.fitconnect.cli.commands.CreateTestKeysCommand;
 import dev.fitko.fitconnect.cli.commands.GetAllSubmissionsCommand;
 import dev.fitko.fitconnect.cli.commands.GetOneSubmissionCommand;
 import dev.fitko.fitconnect.cli.commands.ListAllSubmissionsCommand;
 import dev.fitko.fitconnect.cli.commands.SendBatchCommand;
 import dev.fitko.fitconnect.cli.commands.SendSubmissionCommand;
+import dev.fitko.fitconnect.cli.keygen.JWKGenerator;
+import dev.fitko.fitconnect.client.SenderClient;
+import dev.fitko.fitconnect.client.SubscriberClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.function.Supplier;
 
 class CommandLineClient {
 
@@ -24,20 +27,21 @@ class CommandLineClient {
     private final GetOneSubmissionCommand getOneSubmissionCommand;
     private final GetAllSubmissionsCommand getAllSubmissionsCommand;
     private final SendBatchCommand sendBatchCommand;
-
+    private final CreateTestKeysCommand createTestKeysCommand;
     private final CommandExecutor commandExecutor;
 
     private final JCommander jc;
 
-    CommandLineClient(final SenderClient senderClient, final SubscriberClient subscriberClient) {
+    CommandLineClient(final Supplier<SenderClient> senderSupplier, final Supplier<SubscriberClient> subscriberSupplier) {
 
         sendSubmissionCommand = new SendSubmissionCommand();
         listAllSubmissionsCommand = new ListAllSubmissionsCommand();
         getOneSubmissionCommand = new GetOneSubmissionCommand();
         getAllSubmissionsCommand = new GetAllSubmissionsCommand();
         sendBatchCommand = new SendBatchCommand();
+        createTestKeysCommand = new CreateTestKeysCommand();
 
-        commandExecutor = new CommandExecutor(senderClient, subscriberClient, new CsvImporter());
+        commandExecutor = new CommandExecutor(senderSupplier, subscriberSupplier, new CsvImporter(), new JWKGenerator());
 
         jc = getJCommander();
     }
@@ -62,6 +66,7 @@ class CommandLineClient {
                 .addCommand(getOneSubmissionCommand)
                 .addCommand(getAllSubmissionsCommand)
                 .addCommand(sendBatchCommand)
+                .addCommand(createTestKeysCommand)
                 .build();
     }
 
@@ -82,6 +87,9 @@ class CommandLineClient {
             case SendBatchCommand.SEND_BATCH_CMD_NAME:
                 commandExecutor.sendBatch(sendBatchCommand);
                 break;
+            case CreateTestKeysCommand.CREATE_TEST_KEYS_COMMAND_NAME:
+                commandExecutor.createTestKeys(createTestKeysCommand);
+                break;
             default:
                 break;
         }
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineRunner.java b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineRunner.java
index 68cde0c97f7037f13dac4b7790802adc8696b67c..a8833206125d3861e7218dc92b068cbd6927e6b2 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineRunner.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/CommandLineRunner.java
@@ -2,6 +2,7 @@ package dev.fitko.fitconnect.cli;
 
 import dev.fitko.fitconnect.api.config.ApplicationConfig;
 import dev.fitko.fitconnect.client.SenderClient;
+import dev.fitko.fitconnect.client.SubscriberClient;
 import dev.fitko.fitconnect.client.bootstrap.ApplicationConfigLoader;
 import dev.fitko.fitconnect.client.bootstrap.ClientFactory;
 import org.slf4j.Logger;
@@ -11,38 +12,39 @@ import java.io.BufferedReader;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.file.Path;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 public final class CommandLineRunner {
-
+    private static final Logger LOGGER = LoggerFactory.getLogger(CommandLineRunner.class);
     private static final String LOGO = "/splash_screen_banner.txt";
+
     private static final String DEFAULT_CONFIG_NAME = "config.yml";
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(CommandLineRunner.class);
 
     private CommandLineRunner() {
     }
 
     public static void main(final String[] args) {
         printSplashScreen();
-        getCommandLineClient(loadConfig()).run(args);
+        getCommandLineClient().run(args);
+    }
+
+    private static CommandLineClient getCommandLineClient() {
+        final Supplier<SenderClient> senderClientSupplier = () -> ClientFactory.getSenderClient(loadConfig());
+        final Supplier<SubscriberClient> subscriberClientSupplier = () -> ClientFactory.getSubscriberClient(loadConfig());
+        return new CommandLineClient(senderClientSupplier, subscriberClientSupplier);
     }
 
     private static ApplicationConfig loadConfig() {
         try {
             return ApplicationConfigLoader.loadConfigFromEnvironment();
         } catch (final Exception e) {
-            LOGGER.warn("Could not load config from environment, loading default config {}", DEFAULT_CONFIG_NAME);
+            LOGGER.info("Loading config from default location {}", DEFAULT_CONFIG_NAME);
             return ApplicationConfigLoader.loadConfigFromPath(Path.of(DEFAULT_CONFIG_NAME));
         }
     }
 
-    private static CommandLineClient getCommandLineClient(final ApplicationConfig config) {
-        final SenderClient senderClient = ClientFactory.getSenderClient(config);
-        final var subscriberClient = ClientFactory.getSubscriberClient(config);
-        return new CommandLineClient(senderClient, subscriberClient);
-    }
-
     private static void printSplashScreen() {
         LOGGER.info("{}", getSplashScreenResource());
     }
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/CreateTestKeysCommand.java b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/CreateTestKeysCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..4901e7e49d619e475801f389febfc73366710651
--- /dev/null
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/CreateTestKeysCommand.java
@@ -0,0 +1,20 @@
+package dev.fitko.fitconnect.cli.commands;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+
+@Parameters(
+        commandNames = {CreateTestKeysCommand.CREATE_TEST_KEYS_COMMAND_NAME},
+        commandDescription = "Generates JWK test keys for encryption, decryption, signing and signature validation",
+        separators = "="
+)
+public class CreateTestKeysCommand {
+
+    public static final String CREATE_TEST_KEYS_COMMAND_NAME = "keygen";
+
+    @Parameter(names = {"--outDir"}, description = "Output directory folder where the generated test keys are written to", arity = 1)
+    public String outputDir;
+
+    @Parameter(names = {"--withConfig"}, description = "Generates config.yaml with paths of the generated keys", arity = 1)
+    public boolean generateConfig;
+}
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetAllSubmissionsCommand.java b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetAllSubmissionsCommand.java
index 1bd6e91fc1b91184fdb961df66c74d045c0f3bae..a7c1c26e96cb2453596f6341fca8b98f951017a6 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetAllSubmissionsCommand.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetAllSubmissionsCommand.java
@@ -17,10 +17,8 @@ public class GetAllSubmissionsCommand {
     public static final String GET_ALL_CMD_NAME = "all";
 
     @Parameter(names = { "--destinationId" }, description = "Unique destination identifier in UUID format", converter = UUIDConverter.class, validateWith = UUIDValidator.class, arity = 1, required = true)
-    public
-    UUID destinationId;
+    public UUID destinationId;
 
     @Parameter(names = { "--target" }, description = "Target folder where attachments and data is written to", arity = 1)
-    public
-    String targetFolder;
+    public String targetFolder;
 }
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetOneSubmissionCommand.java b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetOneSubmissionCommand.java
index bc573146b9a1d05419f81d1ffe94c595840875f4..d5e50ccb1a3034b4d92e95da0a238b04b77e5066 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetOneSubmissionCommand.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/GetOneSubmissionCommand.java
@@ -18,10 +18,8 @@ public class GetOneSubmissionCommand {
     public static final String GET_CMD_NAME = "get";
 
     @Parameter(names = { "--submissionId" }, description = "Unique submission identifier in UUID format", converter = UUIDConverter.class, validateWith = UUIDValidator.class, arity = 1, required = true)
-    public
-    UUID submissionId;
+    public UUID submissionId;
 
     @Parameter(names = { "--target" }, description = "Target folder where attachments and data is written to", arity = 1)
-    public
-    String targetFolder;
+    public String targetFolder;
 }
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/SendSubmissionCommand.java b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/SendSubmissionCommand.java
index d4573beb6fa7c32fa8056d27fca59dbfa3878dbf..4cce06bd5312a4313d9c3e073c5815dac483c68f 100644
--- a/cli/src/main/java/dev/fitko/fitconnect/cli/commands/SendSubmissionCommand.java
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/commands/SendSubmissionCommand.java
@@ -27,27 +27,21 @@ public class SendSubmissionCommand {
     UUID destinationId;
 
     @Parameter(names = {"--serviceName"}, arity = 1, description = "Name of the service type", required = true)
-    public
-    String serviceName;
+    public String serviceName;
 
     @Parameter(names = {"--leikaKey"}, arity = 1, description = "The LeikaKey of the service type", required = true)
-    public
-    String leikaKey;
+    public String leikaKey;
 
     @Parameter(names = {"--data"}, arity = 1, description = "Path to JSON or XML data", required = true)
-    public
-    String data;
+    public String data;
 
     @Parameter(names = {"--dataType"}, arity = 1, description = "Data mime type (json/xml), default JSON", required = true)
-    public
-    AttachmentDataType dataType;
+    public AttachmentDataType dataType;
 
     @Parameter(names = "--attachments", description = "Attachments as list of paths", variableArity = true)
-    public
-    List<String> attachments = Collections.emptyList();
+    public List<String> attachments = Collections.emptyList();
 
     @Parameter(names = "--schemaUri", description = "Schema URI to validate submission data", required = true)
-    public
-    String schemaUri;
+    public String schemaUri;
 }
 
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKGenerator.java b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..f38ba5f8ba8091780313bade25e2d6af1981a4bd
--- /dev/null
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKGenerator.java
@@ -0,0 +1,130 @@
+package dev.fitko.fitconnect.cli.keygen;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.KeyOperation;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.Base64;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import java.math.BigInteger;
+import java.security.InvalidParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * JWK Test Key Generator.
+ * <p>
+ * Generates key pairs of public and private keys for encryption and signing.
+ */
+public class JWKGenerator {
+
+    public static final int DEFAULT_KEY_SIZE = 4096;
+
+    /**
+     * Generate a set of public encryption key and private decryption key.
+     *
+     * @param keySize size of the RSA key in bits
+     * @return JWKPair of public and private key
+     */
+    public JWKPair generateEncryptionKeyPair(final int keySize) {
+        final KeyPair keyPair = getKeyPair(keySize);
+        final List<Base64> x509CertChain = getX509CertChain(keyPair);
+
+        final String keyId = UUID.randomUUID().toString();
+        final JWEAlgorithm encryptionAlgorithm = JWEAlgorithm.RSA_OAEP_256;
+
+        final RSAKey publicEncryptionKey = buildRSAKey(keyId, keyPair, KeyOperation.WRAP_KEY, encryptionAlgorithm, x509CertChain);
+        final RSAKey privateDecryptionKey = buildRSAKey(keyId, keyPair, KeyOperation.UNWRAP_KEY, encryptionAlgorithm);
+
+        return new JWKPair(publicEncryptionKey.toPublicJWK(), privateDecryptionKey);
+    }
+
+    /**
+     * Generate a set of public signature verification key and private signature key.
+     *
+     * @param keySize size of the RSA key in bits
+     * @return JWKPair of signature and verification key
+     */
+    public JWKPair generateSignatureKeyPair(final int keySize) {
+        final KeyPair keyPair = getKeyPair(keySize);
+        final List<Base64> x509CertChain = getX509CertChain(keyPair);
+
+        final String keyId = UUID.randomUUID().toString();
+        final JWSAlgorithm signingAlgorithm = JWSAlgorithm.PS512;
+
+        final RSAKey privateSignatureKey = buildRSAKey(keyId, keyPair, KeyOperation.SIGN, signingAlgorithm);
+        final RSAKey publicSignatureVerificationKey = buildRSAKey(keyId, keyPair, KeyOperation.VERIFY, signingAlgorithm, x509CertChain);
+
+        return new JWKPair(publicSignatureVerificationKey.toPublicJWK(), privateSignatureKey);
+    }
+
+    private static List<Base64> getX509CertChain(final KeyPair keyPair) {
+        final X509Certificate cert = getX509Certificate(keyPair);
+        try {
+            return RSAKey.parse(cert).getX509CertChain();
+        } catch (final JOSEException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static RSAKey buildRSAKey(final String keyId, final KeyPair keyPair, final KeyOperation keyOperation, final Algorithm algorithm, final List<Base64> x509CertChain) {
+        final RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
+                .privateKey(keyPair.getPrivate())
+                .keyID(keyId)
+                .keyOperations(Set.of(keyOperation))
+                .algorithm(algorithm);
+        return x509CertChain.isEmpty() ? builder.build() : builder.x509CertChain(x509CertChain).build();
+    }
+
+    private static RSAKey buildRSAKey(final String keyId, final KeyPair keyPair, final KeyOperation keyOperation, final Algorithm algorithm) {
+        return buildRSAKey(keyId, keyPair, keyOperation, algorithm, Collections.emptyList());
+    }
+
+    private static X509Certificate getX509Certificate(final KeyPair keyPair) {
+        final Instant now = Instant.now();
+        final Date notBefore = Date.from(now);
+        final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10)));
+
+        try {
+            final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512withRSA").build(keyPair.getPrivate());
+            final X500Name x500Name = new X500Name("CN=localhost");
+            final X500Name x500Subject = new X500Name("C=Test");
+            final BigInteger serialNr = BigInteger.valueOf(now.toEpochMilli());
+            final X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(x500Name, serialNr, notBefore, notAfter, x500Subject, keyPair.getPublic());
+            return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner));
+        } catch (final CertificateException | OperatorCreationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static KeyPair getKeyPair(final int keySize) {
+        try {
+            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+            keyPairGenerator.initialize(keySize);
+            return keyPairGenerator.generateKeyPair();
+        } catch (final NoSuchAlgorithmException | InvalidParameterException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKPair.java b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKPair.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd723a45163d8f148ce90df2e0b33975ba587274
--- /dev/null
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/JWKPair.java
@@ -0,0 +1,11 @@
+package dev.fitko.fitconnect.cli.keygen;
+
+import com.nimbusds.jose.jwk.JWK;
+import lombok.Value;
+
+@Value
+public class JWKPair {
+
+    JWK publicKey;
+    JWK privateKey;
+}
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriter.java b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..84fca07628638ef5a1ab7e1ac1fbc152797cad44
--- /dev/null
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriter.java
@@ -0,0 +1,105 @@
+package dev.fitko.fitconnect.cli.keygen;
+
+import com.nimbusds.jose.jwk.JWK;
+import dev.fitko.fitconnect.api.config.defaults.DefaultEnvironments;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class KeyWriter {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(KeyWriter.class);
+
+    public static final String PUBLIC_ENCRYPTION_KEY_NAME = "publicKey_encryption.json";
+    public static final String PUBLIC_ENCRYPTION_KEY_DESC = "Encryption Public Key (key_use=wrapKey)";
+    public static final String PRIVATE_DECRYPTION_KEY_NAME = "privateKey_decryption.json";
+    public static final String PRIVATE_DECRYPTION_KEY_DESC = "Decryption Private Key (key_use=unwrapKey)";
+    public static final String PUBLIC_SIGNATURE_VERIFICATION_KEY_NAME = "publicKey_signature_verification.json";
+    public static final String PUBLIC_SIGNATURE_VERIFICATION_KEY_DESC = "Signature Verification Public Key (key_use=verify)";
+    public static final String PRIVATE_SIGNING_KEY_NAME = "privateKey_signing.json";
+    public static final String PRIVATE_SIGNING_KEY_DESC = "Signing Private Key (key_use=sign)";
+    public static final String TEMP_DIR_NAME = "testJWKs";
+
+    public void writeKeys(final KeyWriterSettings settings) {
+
+        final String dir = getKeyDirectory(settings.getOutputDir());
+        LOGGER.info("Writing keys to directory {}", dir);
+
+        final JWKPair encryptionKeyPair = settings.getEncryptionKeyPair();
+        final JWKPair signatureKeyPair = settings.getSignatureKeyPair();
+
+        writeKeyToFile(dir, PUBLIC_ENCRYPTION_KEY_NAME, PUBLIC_ENCRYPTION_KEY_DESC, encryptionKeyPair.getPublicKey());
+        writeKeyToFile(dir, PRIVATE_DECRYPTION_KEY_NAME, PRIVATE_DECRYPTION_KEY_DESC, encryptionKeyPair.getPrivateKey());
+
+        writeKeyToFile(dir, PUBLIC_SIGNATURE_VERIFICATION_KEY_NAME, PUBLIC_SIGNATURE_VERIFICATION_KEY_DESC, signatureKeyPair.getPublicKey());
+        writeKeyToFile(dir, PRIVATE_SIGNING_KEY_NAME, PRIVATE_SIGNING_KEY_DESC, signatureKeyPair.getPrivateKey());
+
+        if (settings.isCreateConfigYaml()) {
+            writeConfigYamlToFile(dir);
+        }
+    }
+
+    private String getKeyDirectory(final String outputDir) {
+        if (outputDir == null) {
+            try {
+                return Files.createTempDirectory(TEMP_DIR_NAME).toFile().getAbsolutePath();
+            } catch (final IOException e) {
+                LOGGER.error(e.getMessage(), e);
+                System.exit(0);
+            }
+        }
+        return Path.of(outputDir).toAbsolutePath().toString();
+    }
+
+    private void writeConfigYamlToFile(final String dir) {
+
+        final Map<String, Object> data = new LinkedHashMap<>();
+        data.put("senderConfig",
+                Map.of("clientSecret", "",
+                        "clientId", ""));
+        data.put("subscriberConfig",
+                Map.of("clientSecret", "",
+                        "clientId", "",
+                        "privateDecryptionKeyPaths", List.of(Path.of(dir, PRIVATE_DECRYPTION_KEY_NAME).toAbsolutePath().toString()),
+                        "privateSigningKeyPath", Path.of(dir,PRIVATE_SIGNING_KEY_NAME).toAbsolutePath().toString()));
+        data.put("activeEnvironment",  DefaultEnvironments.TEST.getEnvironmentName().getName());
+
+        final DumperOptions options = new DumperOptions();
+        options.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
+        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+        options.setPrettyFlow(true);
+
+        final StringWriter writer = new StringWriter();
+        final Yaml yaml = new Yaml(options);
+        yaml.dump(data, writer);
+        writer.flush();
+
+        writeFile(Path.of(dir, "config.yml"), writer.toString());
+
+        LOGGER.info("Wrote config.yml");
+    }
+
+    private void writeKeyToFile(final String dir, final String filename, final String desc, final JWK jwk) {
+        writeFile(Path.of(dir, filename), jwk.toJSONString());
+        LOGGER.info("Wrote {} as {}", desc, filename);
+    }
+
+    private void writeFile(final Path path, final String content) {
+        try {
+            Files.write(path, content.getBytes(StandardCharsets.UTF_8));
+        } catch (final IOException e) {
+            LOGGER.error(e.getMessage(), e);
+            System.exit(0);
+        }
+    }
+}
diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriterSettings.java b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriterSettings.java
new file mode 100644
index 0000000000000000000000000000000000000000..2fab1de5690e91a30a918dd9429f33791b7a3b23
--- /dev/null
+++ b/cli/src/main/java/dev/fitko/fitconnect/cli/keygen/KeyWriterSettings.java
@@ -0,0 +1,15 @@
+package dev.fitko.fitconnect.cli.keygen;
+
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder
+public class KeyWriterSettings {
+
+    String outputDir;
+    boolean createConfigYaml;
+
+    JWKPair encryptionKeyPair;
+    JWKPair signatureKeyPair;
+}
diff --git a/client/src/main/resources/splash_screen_banner.txt b/cli/src/main/resources/splash_screen_banner.txt
similarity index 71%
rename from client/src/main/resources/splash_screen_banner.txt
rename to cli/src/main/resources/splash_screen_banner.txt
index 4e4892d0d632503223d08b946eb840ee00ec3074..b45dff53c9f452d0e10dd5053b043613b600f68d 100644
--- a/client/src/main/resources/splash_screen_banner.txt
+++ b/cli/src/main/resources/splash_screen_banner.txt
@@ -1,7 +1,8 @@
 
-    ________________    ______                            __     _________            __
-   / ____/  _/_  __/   / ____/___  ____  ____  ___  _____/ /_   / ____/ (_)__  ____  / /_
-  / /_   / /  / /_____/ /   / __ \/ __ \/ __ \/ _ \/ ___/ __/  / /   / / / _ \/ __ \/ __/
- / __/ _/ /  / /_____/ /___/ /_/ / / / / / / /  __/ /__/ /_   / /___/ / /  __/ / / / /_  
-/_/   /___/ /_/      \____/\____/_/ /_/_/ /_/\___/\___/\__/   \____/_/_/\___/_/ /_/\__/  
+
+    ________________    ______                            __     ________    ____
+   / ____/  _/_  __/   / ____/___  ____  ____  ___  _____/ /_   / ____/ /   /  _/
+  / /_   / /  / /_____/ /   / __ \/ __ \/ __ \/ _ \/ ___/ __/  / /   / /    / /
+ / __/ _/ /  / /_____/ /___/ /_/ / / / / / / /  __/ /__/ /_   / /___/ /____/ /
+/_/   /___/ /_/      \____/\____/_/ /_/_/ /_/\___/\___/\__/   \____/_____/___/
 
diff --git a/cli/src/test/java/dev/fitko/fitconnect/cli/CommandLineClientTest.java b/cli/src/test/java/dev/fitko/fitconnect/cli/CommandLineClientTest.java
index 4a2ac3030c355d9baec310e6361d585b3e0b999f..71e1e09ed478847e63206f66c45e8ffaa6943bf5 100644
--- a/cli/src/test/java/dev/fitko/fitconnect/cli/CommandLineClientTest.java
+++ b/cli/src/test/java/dev/fitko/fitconnect/cli/CommandLineClientTest.java
@@ -6,20 +6,21 @@ 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.services.Subscriber;
+import dev.fitko.fitconnect.cli.batch.CsvImporter;
+import dev.fitko.fitconnect.cli.batch.ImportRecord;
+import dev.fitko.fitconnect.cli.keygen.KeyWriter;
 import dev.fitko.fitconnect.client.SenderClient;
 import dev.fitko.fitconnect.client.SubscriberClient;
-
 import dev.fitko.fitconnect.client.sender.model.Attachment;
 import dev.fitko.fitconnect.client.sender.model.SendableEncryptedSubmission;
 import dev.fitko.fitconnect.client.sender.model.SendableSubmission;
 import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
 import dev.fitko.fitconnect.client.subscriber.model.ReceivedData;
-import dev.fitko.fitconnect.cli.batch.CsvImporter;
-import dev.fitko.fitconnect.cli.batch.ImportRecord;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashSet;
 import java.util.List;
@@ -27,6 +28,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -45,7 +47,7 @@ class CommandLineClientTest {
         senderClientMock = mock(SenderClient.class);
         subscriberMock = mock(Subscriber.class);
         subscriberClientMock = mock(SubscriberClient.class);
-        underTest = new CommandLineClient(senderClientMock, subscriberClientMock);
+        underTest = new CommandLineClient(() -> senderClientMock, () -> subscriberClientMock);
     }
 
     @Test
@@ -127,7 +129,6 @@ class CommandLineClientTest {
     void testBatchImport() {
 
         // Given
-
         final var testDataPath = "src/test/resources/batch_data.csv";
         final List<ImportRecord> importRecords = new CsvImporter().readRecords(testDataPath);
 
@@ -141,6 +142,31 @@ class CommandLineClientTest {
         logs.assertContains("DONE ! Finished batch import of " + importRecords.size() + " submissions");
     }
 
+    @Test
+    void testKeyGeneration(@TempDir final Path tempDir) {
+
+        // When
+        underTest.run("keygen", "--outDir=" + tempDir.toAbsolutePath());
+
+        // Then
+        logs.assertContains("Writing keys to directory " + tempDir.toAbsolutePath());
+
+        assertTrue(Files.exists(Path.of(tempDir.toString(), KeyWriter.PUBLIC_ENCRYPTION_KEY_NAME)));
+        assertTrue(Files.exists(Path.of(tempDir.toString(), KeyWriter.PRIVATE_DECRYPTION_KEY_NAME)));
+        assertTrue(Files.exists(Path.of(tempDir.toString(), KeyWriter.PUBLIC_SIGNATURE_VERIFICATION_KEY_NAME)));
+        assertTrue(Files.exists(Path.of(tempDir.toString(), KeyWriter.PRIVATE_SIGNING_KEY_NAME)));
+    }
+
+    @Test
+    void testKeyGenerationWithConfigOption(@TempDir final Path tempDir) {
+
+        // When
+        underTest.run("keygen", "--outDir=" + tempDir.toAbsolutePath(), "--withConfig=true");
+
+        // Then
+        assertTrue(Files.exists(Path.of(tempDir.toString(), "config.yml")));
+    }
+
     private Set<SubmissionForPickup> generateSubmissions(final UUID destinationId, final int count) {
         final Set<SubmissionForPickup> submissions = new HashSet<>(count);
         for (int i = 0; i < count; i++) {
diff --git a/cli/src/test/java/dev/fitko/fitconnect/cli/keygen/JWKGeneratorTest.java b/cli/src/test/java/dev/fitko/fitconnect/cli/keygen/JWKGeneratorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..772162da281618efa7ceb8da6563f43897daebf9
--- /dev/null
+++ b/cli/src/test/java/dev/fitko/fitconnect/cli/keygen/JWKGeneratorTest.java
@@ -0,0 +1,199 @@
+package dev.fitko.fitconnect.cli.keygen;
+
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.KeyOperation;
+import com.nimbusds.jose.jwk.KeyType;
+import dev.fitko.fitconnect.core.crypto.JWECryptoService;
+import dev.fitko.fitconnect.jwkvalidator.JWKValidator;
+import dev.fitko.fitconnect.jwkvalidator.exceptions.LogLevel;
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class JWKGeneratorTest {
+
+    private JWKGenerator underTest;
+
+    @BeforeEach
+    void setup() {
+        underTest = new JWKGenerator();
+    }
+
+    @Test
+    void testPublicEncryptionKey() {
+
+        // When
+        final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(2048);
+
+        final JWK publicKey = encryptionKeyPair.getPublicKey();
+
+        // Then
+        assertThat(publicKey, CoreMatchers.is(notNullValue()));
+
+
+        assertThat(publicKey.getKeyID(), CoreMatchers.is(notNullValue()));
+
+        assertThat(publicKey.getKeyOperations(), hasSize(1));
+        assertThat(publicKey.getKeyOperations(), contains(KeyOperation.WRAP_KEY));
+
+        assertThat(publicKey.getX509CertChain(), hasSize(1));
+
+        assertThat(publicKey.getKeyType(), CoreMatchers.is(KeyType.RSA));
+        assertThat( publicKey.getAlgorithm(), CoreMatchers.is(JWEAlgorithm.RSA_OAEP_256));
+    }
+
+    @Test
+    void testPrivateDecryptionKey() {
+
+        // When
+        final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(2048);
+
+        final JWK privateKey = encryptionKeyPair.getPrivateKey();
+
+        // Then
+        assertThat(privateKey, CoreMatchers.is(CoreMatchers.notNullValue()));
+
+        assertThat(privateKey.getKeyID(), CoreMatchers.is(notNullValue()));
+
+        assertThat(privateKey.getKeyOperations(), hasSize(1));
+        assertThat(privateKey.getKeyOperations(), contains(KeyOperation.UNWRAP_KEY));
+
+        assertThat(privateKey.getX509CertChain(), CoreMatchers.is(nullValue()));
+
+        assertThat(privateKey.getKeyType(), CoreMatchers.is(KeyType.RSA));
+        assertThat(privateKey.getAlgorithm(), CoreMatchers.is(JWEAlgorithm.RSA_OAEP_256));
+
+        final Map<String, Object> keyParams = privateKey.toRSAKey().toJSONObject();
+
+        assertThat(keyParams.get("d"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("dp"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("dq"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("e"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("n"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("p"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("q"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("qi"), CoreMatchers.is(CoreMatchers.notNullValue()));
+    }
+
+    @Test
+    void testEncryptionAndDecryption() {
+
+        // Given
+        final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(4096);
+
+        final JWK publicKey = encryptionKeyPair.getPublicKey();
+        final JWK privateKey = encryptionKeyPair.getPrivateKey();
+
+        final var data = "test string to encrypt";
+
+        // When
+        final JWECryptoService cryptoService = new JWECryptoService(null);
+
+        final String encryptedData = cryptoService.encryptBytes(publicKey.toRSAKey(), data.getBytes(StandardCharsets.UTF_8));
+        final byte[] decryptedData = cryptoService.decryptToBytes(privateKey.toRSAKey(), encryptedData);
+
+        // Then
+        assertThat(data, is(new String(decryptedData)));
+    }
+
+    @Test
+    void testPublicEncryptionKeyValidationWithCorrectKeyLength() {
+
+        // Given
+        final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(4096);
+
+        final JWK publicKey = encryptionKeyPair.getPublicKey();
+
+        // Then
+        assertDoesNotThrow(() -> JWKValidator.withoutX5CValidation()
+                .withErrorLogLevel(LogLevel.ERROR)
+                .build()
+                .validate(publicKey.toRSAKey(), KeyOperation.WRAP_KEY));
+    }
+
+    @Test
+    void testPublicSignatureVerificationKey() {
+
+        // When
+        final JWKPair signatureKeyPair = underTest.generateSignatureKeyPair(2048);
+
+        final JWK publicKey = signatureKeyPair.getPublicKey();
+
+        // Then
+        assertThat(publicKey, CoreMatchers.is(CoreMatchers.notNullValue()));
+
+        assertThat(publicKey.getKeyID(), CoreMatchers.is(notNullValue()));
+
+        assertThat(publicKey.getKeyOperations(), hasSize(1));
+        assertThat(publicKey.getKeyOperations(), contains(KeyOperation.VERIFY));
+
+        assertThat(publicKey.getX509CertChain(), hasSize(1));
+
+        assertThat(publicKey.getKeyType(), CoreMatchers.is(KeyType.RSA));
+        assertThat( publicKey.getAlgorithm(), CoreMatchers.is(JWSAlgorithm.PS512));
+
+    }
+
+    @Test
+    void testPublicSignatureKeyValidation() {
+
+        // Given
+        final JWKPair signatureKeyPair = underTest.generateSignatureKeyPair(4096);
+
+        final JWK publicKey = signatureKeyPair.getPublicKey();
+
+        // Then
+        assertDoesNotThrow(() -> JWKValidator.withoutX5CValidation()
+                .withErrorLogLevel(LogLevel.ERROR)
+                .build()
+                .validate(publicKey.toRSAKey(), KeyOperation.VERIFY));
+    }
+
+    @Test
+    void testPrivateSigningKey() {
+
+        // When
+        final JWKPair signatureKeyPair = underTest.generateSignatureKeyPair(2048);
+
+        final JWK privateKey =  signatureKeyPair.getPrivateKey();
+
+        // Then
+        assertThat(privateKey, CoreMatchers.is(notNullValue()));
+
+
+        assertThat(privateKey.getKeyID(), CoreMatchers.is(notNullValue()));
+
+        assertThat(privateKey.getKeyOperations(), hasSize(1));
+        assertThat(privateKey.getKeyOperations(), contains(KeyOperation.SIGN));
+
+        assertThat(privateKey.getX509CertChain(), CoreMatchers.is(nullValue()));
+
+        assertThat(privateKey.getKeyType(), CoreMatchers.is(KeyType.RSA));
+        assertThat(privateKey.getAlgorithm(), CoreMatchers.is(JWSAlgorithm.PS512));
+
+        final Map<String, Object> keyParams = privateKey.toRSAKey().toJSONObject();
+
+        assertThat(keyParams.get("d"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("dp"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("dq"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("e"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("n"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("p"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("q"), CoreMatchers.is(CoreMatchers.notNullValue()));
+        assertThat(keyParams.get("qi"), CoreMatchers.is(CoreMatchers.notNullValue()));
+
+    }
+}
\ No newline at end of file
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/steps/BuilderStartStep.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/steps/BuilderStartStep.java
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
index b1af51fa937b7a6ba67dbf85462c4edd0704d6c4..c472e83ffd3608eab1657046dccfe6a19d42942f 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
@@ -59,9 +59,7 @@ public class SendNewSubmissionStrategy {
         final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission);
         final UUID announcedSubmissionId = announcedSubmission.getSubmissionId();
 
-        final var startTimeAttachmentUpload = StopWatch.start();
         uploadAttachments(encryptedAttachments, announcedSubmissionId);
-        LOGGER.info("Uploading attachments took {}", StopWatch.stopWithFormattedTime(startTimeAttachmentUpload));
 
         LOGGER.info("Creating metadata");
         final Metadata metadata = buildMetadata(sendableSubmission, encryptedAttachments);
@@ -155,7 +153,9 @@ public class SendNewSubmissionStrategy {
             LOGGER.info("No attachments to upload");
         } else {
             LOGGER.info("Uploading {} attachment(s)", attachmentPayloads.size());
+            final var startTimeAttachmentUpload = StopWatch.start();
             attachmentPayloads.forEach(a -> sender.uploadAttachment(submissionId, a.getAttachmentId(), a.getEncryptedData()));
+            LOGGER.info("Uploading attachments took {}", StopWatch.stopWithFormattedTime(startTimeAttachmentUpload));
         }
     }
 
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
index a791dde8ce7c65514c9e3bab74bf904944e7406a..b4bba3de097343e16eee9da72368e93eaa2b9a7a 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
@@ -55,7 +55,7 @@ public class SchemaResourceProvider implements SchemaProvider {
         populateDestinationSchemas(schemaResources.getDestinationSchemaPaths());
         populateSubmissionDataSchemas(schemaResources.getSubmissionDataSchemaPath());
 
-        LOGGER.info("Initialised sdk schemas");
+        LOGGER.info("Initialised SDK schemas");
     }
 
     private void populateMetadataSchemas(final List<String> metadataSchemaPaths) {
diff --git a/pom.xml b/pom.xml
index 60704b78d9fc65a785b52c66d36dce80240b58b3..28c3d1957ddcebfa47799afc544214f077d3f7cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,7 +110,6 @@
         <module>api</module>
         <module>core</module>
         <module>client</module>
-        <module>demo</module>
     </modules>
 
     <dependencyManagement>
@@ -135,6 +134,12 @@
                 <version>${project.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>dev.fitko.fitconnect.sdk</groupId>
+                <artifactId>cli</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>dev.fitko.fitconnect.sdk</groupId>
                 <artifactId>integration-tests</artifactId>
@@ -488,6 +493,15 @@
                 <module>integration-tests</module>
             </modules>
         </profile>
+        <profile>
+            <id>cli-tests</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <modules>
+                <module>cli</module>
+            </modules>
+        </profile>
         <profile>
             <id>demo</id>
             <activation>