Skip to content

Commit c0ffeeb

Browse files
EPA-266: Allow custom truststores for Konnektors - Some do not use TI TSL
1 parent 9111d6f commit c0ffeeb

15 files changed

Lines changed: 390 additions & 69 deletions

File tree

-6.34 KB
Binary file not shown.
-1.86 KB
Binary file not shown.

epa4all-rest-service/README.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Small wrapper around the epa4all client library.
77
**[openapi.yaml](./src/main/resources/META-INF/openapi/openapi.yaml)**
88

99
#### Example
10+
1011
```shell
1112
# Add a new FHIR document
1213
curl -X 'POST' \
@@ -58,20 +59,23 @@ docker run --rm \
5859

5960
## Configuration Options
6061

61-
| name | description | default |
62-
|---------------------------------|-----------------------------------------------------------------------------------------|--------------|
63-
| `EPA4ALL_LOG_LEVEL`* | Log level for the entire application. | `INFO` |
64-
| `EPA4ALL_ADDRESS`* | Address to bind the server to. | `0.0.0.0` |
65-
| `EPA4ALL_PORT`* | Port to bind the server to. | `8080` |
66-
| `EPA4ALL_KONNEKTOR_URI`* | URI of the Konnektor to use, e.g. `https://10.1.2.3:443`. | |
67-
| `EPA4ALL_PROXY_ADDRESS`* | Address of the forward proxy infront of the Konnektor, e.g. `127.0.0.1`. | |
68-
| `EPA4ALL_PROXY_PORT`* | Port of the forward proxy infront of the Konnektor. | `3128` |
69-
| `EPA4ALL_CREDENTIALS_PATH`* | The PKCS#12 keystore containing the TLS client certificate to connect to the Konnektor. | `./credentials.p12` |
70-
| `EPA4ALL_CREDENTIALS_PASSWORD`* | The password of the PKCS#12 keystore containing the TLS client certificate. | `0000` |
71-
| `EPA4ALL_WORKPLACE_ID`* | The workplace ID configured in the Konnektor. | `a` |
72-
| `EPA4ALL_CLIENT_SYSTEM_ID`* | The client system ID configured in the Konnektor. | `c` |
73-
| `EPA4ALL_MANDANT_ID`* | The mandant ID configured in the Konnektor. | `m` |
74-
| `EPA4ALL_USER_ID`* | The user ID configured in the Konnektor. | `admin` |
75-
| `EPA4ALL_ENVIRONMENT`* | The telematik environment, either RU or PU. | `PU` |
76-
| `EPA4ALL_TELEMETRY_OPTOUT` | Basic telemetry to help with development. | `false` |
77-
| `EPA4ALL_TELEMATIK_ID` | The telematik id of the SMC-B card | |
62+
| name | description | default |
63+
|-----------------------------------------|---------------------------------------------------------------------------------------------|---------------------------------|
64+
| `EPA4ALL_LOG_LEVEL`* | Log level for the entire application. | `INFO` |
65+
| `EPA4ALL_ADDRESS`* | Address to bind the server to. | `0.0.0.0` |
66+
| `EPA4ALL_PORT`* | Port to bind the server to. | `8080` |
67+
| `EPA4ALL_KONNEKTOR_URI`* | URI of the Konnektor to use, e.g. `https://10.1.2.3:443`. | |
68+
| `EPA4ALL_KONNEKTOR_SERVERNAME` | The servername of the Konnektor as used and validated in its TLS certificate. | `konnektor.konlan` |
69+
| `EPA4ALL_KONNEKTOR_TRUSTSTORE_PATH` | The PKCS#12 keystore containing the TLS trust-anchors for the Konnektor server certificate. | built-in trust-anchors from TSL |
70+
| `EPA4ALL_KONNEKTOR_TRUSTSTORE_PASSWORD` | Password for the PKCS#12 truststore. | `changeit` |
71+
| `EPA4ALL_PROXY_ADDRESS`* | Address of the forward proxy infront of the Konnektor, e.g. `127.0.0.1`. | |
72+
| `EPA4ALL_PROXY_PORT`* | Port of the forward proxy infront of the Konnektor. | `3128` |
73+
| `EPA4ALL_CREDENTIALS_PATH`* | The PKCS#12 keystore containing the TLS client certificate to connect to the Konnektor. | `./credentials.p12` |
74+
| `EPA4ALL_CREDENTIALS_PASSWORD`* | The password of the PKCS#12 keystore containing the TLS client certificate. | `0000` |
75+
| `EPA4ALL_WORKPLACE_ID`* | The workplace ID configured in the Konnektor. | `a` |
76+
| `EPA4ALL_CLIENT_SYSTEM_ID`* | The client system ID configured in the Konnektor. | `c` |
77+
| `EPA4ALL_MANDANT_ID`* | The mandant ID configured in the Konnektor. | `m` |
78+
| `EPA4ALL_USER_ID`* | The user ID configured in the Konnektor. | `admin` |
79+
| `EPA4ALL_ENVIRONMENT`* | The telematik environment, either RU or PU. | `PU` |
80+
| `EPA4ALL_TELEMETRY_OPTOUT` | Basic telemetry to help with development. | `false` |
81+
| `EPA4ALL_TELEMATIK_ID` | The telematik id of the SMC-B card | |

epa4all-rest-service/src/main/java/com/oviva/telematik/epa4all/restservice/KeyStores.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.util.List;
1515
import javax.net.ssl.KeyManager;
1616
import javax.net.ssl.KeyManagerFactory;
17+
import javax.net.ssl.TrustManager;
18+
import javax.net.ssl.TrustManagerFactory;
1719

1820
public class KeyStores {
1921

@@ -27,28 +29,40 @@ public static List<KeyManager> loadKeys(@NonNull Path keystoreFile, @Nullable St
2729
var keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
2830
keyFactory.init(ks, password != null ? password.toCharArray() : null);
2931
return Arrays.asList(keyFactory.getKeyManagers());
30-
} catch (NoSuchAlgorithmException
31-
| IOException
32-
| KeyStoreException
33-
| CertificateException
34-
| UnrecoverableKeyException e) {
32+
} catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException e) {
3533
throw new IllegalArgumentException(
3634
"failed to load keys from '%s', absolute path '%s'"
3735
.formatted(keystoreFile, keystoreFile.toAbsolutePath()),
3836
e);
3937
}
4038
}
4139

40+
@Nullable
41+
public static TrustManager convertToTrustManager(@Nullable KeyStore trustStore) {
42+
try {
43+
if (trustStore == null) {
44+
return null;
45+
}
46+
47+
var tmf = TrustManagerFactory.getInstance("PKIX");
48+
tmf.init(trustStore);
49+
return tmf.getTrustManagers()[0];
50+
} catch (KeyStoreException | NoSuchAlgorithmException e) {
51+
throw new IllegalStateException("failed to initialize TrustManager from KeyStore", e);
52+
}
53+
}
54+
4255
@NonNull
43-
private static KeyStore loadPkcs12KeyStore(@NonNull Path keystoreFile, @Nullable String password)
44-
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
56+
public static KeyStore loadPkcs12KeyStore(@NonNull Path keystoreFile, @Nullable String password) {
4557

4658
try (var is = Files.newInputStream(keystoreFile)) {
4759

4860
var keyStore = KeyStore.getInstance("PKCS12");
4961
keyStore.load(is, password != null ? password.toCharArray() : null);
5062

5163
return keyStore;
64+
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) {
65+
throw new IllegalStateException("failed to load keystore: " + keystoreFile, e);
5266
}
5367
}
5468
}

epa4all-rest-service/src/main/java/com/oviva/telematik/epa4all/restservice/Main.java

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ private KonnektorConnectionFactory buildFactory(Config cfg) {
128128
return KonnektorConnectionFactoryBuilder.newBuilder()
129129
.clientKeys(cfg.clientKeys())
130130
.konnektorUri(cfg.konnektorUri())
131+
.konnektorServername(cfg.konnektorServername())
131132
.proxyServer(cfg.proxyAddress(), cfg.proxyPort())
132-
.trustManagers(loadTrustManagers(cfg.environment()))
133+
.trustManagers(cfg.konnektorTruststores())
133134
.build();
134135
}
135136

136-
private List<TrustManager> loadTrustManagers(Environment env) {
137+
private List<TrustManager> loadTelematikTrustManagers(Environment env) {
137138
return switch (env) {
138139
case PU -> List.of(TelematikTrustRoots.createPuTrustManager());
139140
case RU -> List.of(TelematikTrustRoots.createRuTrustManager());
@@ -142,6 +143,8 @@ private List<TrustManager> loadTrustManagers(Environment env) {
142143

143144
record Config(
144145
URI konnektorUri,
146+
String konnektorServername,
147+
List<TrustManager> konnektorTruststores,
145148
String proxyAddress,
146149
int proxyPort,
147150
List<KeyManager> clientKeys,
@@ -156,11 +159,47 @@ record Config(
156159

157160
private Config loadConfig(ConfigProvider configProvider) {
158161

162+
var environment =
163+
configProvider
164+
.get("environment")
165+
.map(String::toUpperCase)
166+
.map(Environment::valueOf)
167+
.orElse(Environment.PU);
168+
159169
var address = configProvider.get("address").orElse("0.0.0.0");
160170
var port = configProvider.get("port").map(Integer::parseInt).orElse(8080);
161171

162172
var uri = mustLoad("konnektor.uri").map(URI::create).orElseThrow();
163173

174+
var servername =
175+
configProvider
176+
.get("konnektor.servername")
177+
.map(String::strip)
178+
.filter(s -> !s.isBlank())
179+
// https://gemspec.gematik.de/docs/gemKPT/gemKPT_Arch_TIP/gemKPT_Arch_TIP_V2.10.0/#TIP1-A_6716
180+
// Der Produkttyp Konnektor MUSS es Clientsystemen ermöglichen, die LAN-seitige
181+
// IP-Adresse des Konnektors durch Abfrage des fest vorgegebenen FQDN "konnektor.konlan"
182+
// aufzulösen.
183+
.orElse("konnektor.konlan");
184+
185+
var konnektorTrustStorePassword =
186+
configProvider
187+
.get("konnektor.truststore.password")
188+
.map(String::strip)
189+
.filter(s -> !s.isBlank())
190+
.orElse("changeit");
191+
192+
var konnektorTruststores =
193+
configProvider
194+
.get("konnektor.truststore.path")
195+
.map(String::strip)
196+
.filter(s -> !s.isBlank())
197+
.map(Path::of)
198+
.map(p -> KeyStores.loadPkcs12KeyStore(p, konnektorTrustStorePassword))
199+
.map(KeyStores::convertToTrustManager)
200+
.map(List::of)
201+
.orElse(loadTelematikTrustManagers(environment));
202+
164203
var proxyAddress = configProvider.get("proxy.address").orElse(null);
165204

166205
var proxyPort = configProvider.get("proxy.port").map(Integer::parseInt).orElse(3128);
@@ -183,13 +222,6 @@ private Config loadConfig(ConfigProvider configProvider) {
183222

184223
var user = configProvider.get("user.id").orElse("admin");
185224

186-
var environment =
187-
configProvider
188-
.get("environment")
189-
.map(String::toUpperCase)
190-
.map(Environment::valueOf)
191-
.orElse(Environment.PU);
192-
193225
var telematikId =
194226
configProvider
195227
.get("telematik.id")
@@ -199,6 +231,8 @@ private Config loadConfig(ConfigProvider configProvider) {
199231

200232
return new Config(
201233
uri,
234+
servername,
235+
konnektorTruststores,
202236
proxyAddress,
203237
proxyPort,
204238
keys,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.oviva.telematik.epa4all.restservice;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.security.KeyStore;
9+
import javax.net.ssl.TrustManager;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.io.TempDir;
12+
13+
class KeyStoresTest {
14+
15+
private static final String TEST_KEYSTORE = "fixtures/test-keystore.p12";
16+
private static final String TEST_KEYSTORE_PASSWORD = "testpass";
17+
private static final String TRUSTSTORE = "fixtures/truststore-tiaas-ru.p12";
18+
private static final String TRUSTSTORE_PASSWORD = "changeit";
19+
20+
@TempDir Path tempDir;
21+
22+
@Test
23+
void loadPkcs12KeyStore_withPassword_succeeds() {
24+
var file = createEmptyPkcs12(tempDir.resolve("ks.p12"), "secret");
25+
26+
var result = KeyStores.loadPkcs12KeyStore(file, "secret");
27+
28+
assertNotNull(result);
29+
}
30+
31+
@Test
32+
void loadPkcs12KeyStore_withNullPassword_succeeds() {
33+
var file = createEmptyPkcs12(tempDir.resolve("ks-no-pw.p12"), null);
34+
35+
var result = KeyStores.loadPkcs12KeyStore(file, null);
36+
37+
assertNotNull(result);
38+
}
39+
40+
@Test
41+
void loadPkcs12KeyStore_realKeystore_loadsEntries() {
42+
var file = resourcePath(TEST_KEYSTORE);
43+
44+
var result = KeyStores.loadPkcs12KeyStore(file, TEST_KEYSTORE_PASSWORD);
45+
46+
assertNotNull(result);
47+
}
48+
49+
@Test
50+
void loadPkcs12KeyStore_fileNotFound_throwsIllegalStateException() {
51+
var missing = tempDir.resolve("missing.p12");
52+
53+
assertThrows(IllegalStateException.class, () -> KeyStores.loadPkcs12KeyStore(missing, "pw"));
54+
}
55+
56+
@Test
57+
void loadPkcs12KeyStore_invalidContent_throwsIllegalStateException() throws IOException {
58+
var bad = tempDir.resolve("bad.p12");
59+
Files.writeString(bad, "not-a-keystore");
60+
61+
assertThrows(IllegalStateException.class, () -> KeyStores.loadPkcs12KeyStore(bad, "pw"));
62+
}
63+
64+
@Test
65+
void loadKeys_emptyKeystoreWithPassword_returnsKeyManagers() {
66+
var file = createEmptyPkcs12(tempDir.resolve("empty.p12"), "pw");
67+
68+
var result = KeyStores.loadKeys(file, "pw");
69+
70+
assertNotNull(result);
71+
}
72+
73+
@Test
74+
void loadKeys_withNullPassword_returnsKeyManagers() {
75+
var file = createEmptyPkcs12(tempDir.resolve("empty-no-pw.p12"), null);
76+
77+
var result = KeyStores.loadKeys(file, null);
78+
79+
assertNotNull(result);
80+
}
81+
82+
@Test
83+
void loadKeys_realKeystoreWithKeys_returnsNonEmptyList() {
84+
var file = resourcePath(TEST_KEYSTORE);
85+
86+
var result = KeyStores.loadKeys(file, TEST_KEYSTORE_PASSWORD);
87+
88+
assertFalse(result.isEmpty());
89+
}
90+
91+
@Test
92+
void loadKeys_fileNotFound_throwsIllegalStateException() {
93+
var missing = tempDir.resolve("missing-keys.p12");
94+
95+
assertThrows(IllegalStateException.class, () -> KeyStores.loadKeys(missing, "pw"));
96+
}
97+
98+
@Test
99+
void convertToTrustManager_nullInput_returnsNull() {
100+
var result = KeyStores.convertToTrustManager(null);
101+
102+
assertNull(result);
103+
}
104+
105+
@Test
106+
void convertToTrustManager_emptyKeyStore_returnsTrustManager() throws Exception {
107+
var ks = KeyStore.getInstance("PKCS12");
108+
ks.load(null, null);
109+
110+
var result = KeyStores.convertToTrustManager(ks);
111+
112+
assertInstanceOf(TrustManager.class, result);
113+
}
114+
115+
@Test
116+
void convertToTrustManager_loadedTrustStore_returnsTrustManager() {
117+
var ks = KeyStores.loadPkcs12KeyStore(resourcePath(TRUSTSTORE), TRUSTSTORE_PASSWORD);
118+
119+
var result = KeyStores.convertToTrustManager(ks);
120+
121+
assertNotNull(result);
122+
}
123+
124+
private static Path createEmptyPkcs12(Path file, String password) {
125+
try {
126+
Files.createDirectories(file.getParent());
127+
var ks = KeyStore.getInstance("PKCS12");
128+
ks.load(null, password != null ? password.toCharArray() : null);
129+
try (var os = Files.newOutputStream(file)) {
130+
ks.store(os, password != null ? password.toCharArray() : null);
131+
}
132+
return file;
133+
} catch (Exception e) {
134+
throw new RuntimeException(e);
135+
}
136+
}
137+
138+
private static Path resourcePath(String name) {
139+
var url = KeyStoresTest.class.getClassLoader().getResource(name);
140+
assertNotNull(url, "test resource not found: " + name);
141+
return Path.of(url.getPath());
142+
}
143+
}

epa4all-rest-service/src/test/java/com/oviva/telematik/epa4all/restservice/MainE2ETest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ void writeDocument() {
5656
.header("Content-Type", "application/json")
5757
.post("/documents")
5858
.then()
59-
.statusCode(200)
6059
.log()
61-
.all();
60+
.ifValidationFails()
61+
.statusCode(200);
6262
}
6363

6464
@Test
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)