From a5e4d488c426e83e2d9c3fe309a9921923a2ceda Mon Sep 17 00:00:00 2001 From: "nathaniel.doef" Date: Tue, 12 Jul 2022 10:18:27 -0400 Subject: [PATCH] - refactor and tests --- .../java/ca/uhn/fhir/context/FhirContext.java | 2 +- .../client/api/IRestfulClientFactory.java | 2 +- .../ca/uhn/fhir/rest/https/KeyStoreInfo.java | 15 --- .../ca/uhn/fhir/rest/https/KeyStoreType.java | 31 ----- .../java/ca/uhn/fhir/rest/https/PathType.java | 17 --- .../fhir/rest/https/TlsAuthentication.java | 22 ---- .../uhn/fhir/rest/https/TrustStoreInfo.java | 9 -- .../java/ca/uhn/fhir/tls/KeyStoreInfo.java | 35 ++++++ .../java/ca/uhn/fhir/tls/KeyStoreType.java | 51 ++++++++ .../main/java/ca/uhn/fhir/tls/PathType.java | 37 ++++++ .../fhir/{rest/https => tls}/StoreInfo.java | 2 +- .../ca/uhn/fhir/tls/TlsAuthentication.java | 42 +++++++ .../java/ca/uhn/fhir/tls/TrustStoreInfo.java | 29 +++++ .../java/ca/uhn/fhir/cli/BaseCommand.java | 6 +- .../client/OkHttpRestfulClientFactory.java | 6 +- .../apache/ApacheRestfulClientFactory.java | 4 +- .../client/impl/RestfulClientFactory.java | 2 +- .../client/tls}/TlsAuthenticationSvc.java | 7 +- .../client/tls/TlsAuthenticationSvcTest.java | 110 ++++++++++++++++++ .../src/test/resources/client-keystore.p12 | Bin 0 -> 4339 bytes .../src/test/resources/client-truststore.p12 | Bin 0 -> 1782 bytes .../src/test/resources/server-keystore.p12 | Bin 0 -> 4408 bytes .../src/test/resources/server-truststore.p12 | Bin 0 -> 1814 bytes .../client/JaxRsRestfulClientFactory.java | 4 +- .../BaseFhirVersionParameterizedTest.java | 2 +- .../BaseRequestGeneratingCommandTestUtil.java | 6 +- .../test/utilities/BaseRestServerHelper.java | 2 +- 27 files changed, 329 insertions(+), 114 deletions(-) delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreInfo.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreType.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/PathType.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthentication.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TrustStoreInfo.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreInfo.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreType.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/PathType.java rename hapi-fhir-base/src/main/java/ca/uhn/fhir/{rest/https => tls}/StoreInfo.java (96%) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TlsAuthentication.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TrustStoreInfo.java rename {hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https => hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/tls}/TlsAuthenticationSvc.java (95%) create mode 100644 hapi-fhir-client/src/test/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvcTest.java create mode 100644 hapi-fhir-client/src/test/resources/client-keystore.p12 create mode 100644 hapi-fhir-client/src/test/resources/client-truststore.p12 create mode 100644 hapi-fhir-client/src/test/resources/server-keystore.p12 create mode 100644 hapi-fhir-client/src/test/resources/server-truststore.p12 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 705e9a65dd7..7681a19ebbe 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.rest.client.api.IBasicClient; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.client.api.IRestfulClientFactory; -import ca.uhn.fhir.rest.https.TlsAuthentication; +import ca.uhn.fhir.tls.TlsAuthentication; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.VersionUtil; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClientFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClientFactory.java index 2338d7a64e5..26d643a3bc1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClientFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClientFactory.java @@ -26,7 +26,7 @@ import java.util.Optional; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.https.TlsAuthentication; +import ca.uhn.fhir.tls.TlsAuthentication; public interface IRestfulClientFactory { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreInfo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreInfo.java deleted file mode 100644 index f46b6aeea6d..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreInfo.java +++ /dev/null @@ -1,15 +0,0 @@ -package ca.uhn.fhir.rest.https; - -public class KeyStoreInfo extends StoreInfo { - - private final char[] myKeyPass; - - public KeyStoreInfo(String theFilePath, String theStorePass, String theKeyPass, String theAlias) { - super(theFilePath, theStorePass, theAlias); - this.myKeyPass = toCharArray(theKeyPass); - } - - public char[] getKeyPass() { - return myKeyPass; - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreType.java deleted file mode 100644 index 8b52daaed96..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/KeyStoreType.java +++ /dev/null @@ -1,31 +0,0 @@ -package ca.uhn.fhir.rest.https; - -import ca.uhn.fhir.i18n.Msg; - -import java.util.Arrays; -import java.util.List; - -public enum KeyStoreType { - - PKCS12("p12", "pfx"), - JKS("jks"); - - private List myFileExtensions; - - KeyStoreType(String... theFileExtensions){ - myFileExtensions = Arrays.asList(theFileExtensions); - } - - public List getFileExtensions() { - return myFileExtensions; - } - - public static KeyStoreType fromFileExtension(String theFileExtension) { - for(KeyStoreType type : KeyStoreType.values()){ - if(type.getFileExtensions().contains(theFileExtension.toLowerCase())){ - return type; - } - } - throw new IllegalArgumentException(Msg.code(2106)+"Invalid KeyStore Type"); - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/PathType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/PathType.java deleted file mode 100644 index c4a693f2fdd..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/PathType.java +++ /dev/null @@ -1,17 +0,0 @@ -package ca.uhn.fhir.rest.https; - -public enum PathType { - - FILE("file://"), - RESOURCE("classpath:"); - - private String myPrefix; - - PathType(String thePrefix) { - myPrefix = thePrefix; - } - - public String getPrefix(){ - return myPrefix; - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthentication.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthentication.java deleted file mode 100644 index 793d95228ef..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthentication.java +++ /dev/null @@ -1,22 +0,0 @@ -package ca.uhn.fhir.rest.https; - -import java.util.Optional; - -public class TlsAuthentication { - - private final Optional myKeyStoreInfo; - private final Optional myTrustStoreInfo; - - public TlsAuthentication(Optional theKeyStoreInfo, Optional theTrustStoreInfo) { - myKeyStoreInfo = theKeyStoreInfo; - myTrustStoreInfo = theTrustStoreInfo; - } - - public Optional getKeyStoreInfo() { - return myKeyStoreInfo; - } - - public Optional getTrustStoreInfo() { - return myTrustStoreInfo; - } -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TrustStoreInfo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TrustStoreInfo.java deleted file mode 100644 index 5c40e7c0ba8..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TrustStoreInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package ca.uhn.fhir.rest.https; - -public class TrustStoreInfo extends StoreInfo{ - - public TrustStoreInfo(String theFilePath, String theStorePass, String theAlias) { - super(theFilePath, theStorePass, theAlias); - } - -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreInfo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreInfo.java new file mode 100644 index 00000000000..c43b8705c62 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreInfo.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.tls; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class KeyStoreInfo extends StoreInfo { + + private final char[] myKeyPass; + + public KeyStoreInfo(String theFilePath, String theStorePass, String theKeyPass, String theAlias) { + super(theFilePath, theStorePass, theAlias); + this.myKeyPass = toCharArray(theKeyPass); + } + + public char[] getKeyPass() { + return myKeyPass; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreType.java new file mode 100644 index 00000000000..cda8c16e250 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/KeyStoreType.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.tls; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.i18n.Msg; + +import java.util.Arrays; +import java.util.List; + +public enum KeyStoreType { + + PKCS12("p12", "pfx"), + JKS("jks"); + + private List myFileExtensions; + + KeyStoreType(String... theFileExtensions){ + myFileExtensions = Arrays.asList(theFileExtensions); + } + + public List getFileExtensions() { + return myFileExtensions; + } + + public static KeyStoreType fromFileExtension(String theFileExtension) { + for(KeyStoreType type : KeyStoreType.values()){ + if(type.getFileExtensions().contains(theFileExtension.toLowerCase())){ + return type; + } + } + throw new IllegalArgumentException(Msg.code(2106)+"Invalid KeyStore Type"); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/PathType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/PathType.java new file mode 100644 index 00000000000..068928844fb --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/PathType.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.tls; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public enum PathType { + + FILE("file://"), + RESOURCE("classpath:"); + + private String myPrefix; + + PathType(String thePrefix) { + myPrefix = thePrefix; + } + + public String getPrefix(){ + return myPrefix; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/StoreInfo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/StoreInfo.java similarity index 96% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/StoreInfo.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/StoreInfo.java index f61ec02829a..84779ea8a6d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/StoreInfo.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/StoreInfo.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.rest.https; +package ca.uhn.fhir.tls; import org.apache.commons.io.FilenameUtils; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TlsAuthentication.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TlsAuthentication.java new file mode 100644 index 00000000000..bcafca9b763 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TlsAuthentication.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.tls; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Optional; + +public class TlsAuthentication { + + private final Optional myKeyStoreInfo; + private final Optional myTrustStoreInfo; + + public TlsAuthentication(Optional theKeyStoreInfo, Optional theTrustStoreInfo) { + myKeyStoreInfo = theKeyStoreInfo; + myTrustStoreInfo = theTrustStoreInfo; + } + + public Optional getKeyStoreInfo() { + return myKeyStoreInfo; + } + + public Optional getTrustStoreInfo() { + return myTrustStoreInfo; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TrustStoreInfo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TrustStoreInfo.java new file mode 100644 index 00000000000..10f6bdefe00 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/tls/TrustStoreInfo.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.tls; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class TrustStoreInfo extends StoreInfo{ + + public TrustStoreInfo(String theFilePath, String theStorePass, String theAlias) { + super(theFilePath, theStorePass, theAlias); + } + +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java index 76c89a37097..9eb85dfa1a0 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java @@ -26,9 +26,9 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; -import ca.uhn.fhir.rest.https.TlsAuthentication; -import ca.uhn.fhir.rest.https.KeyStoreInfo; -import ca.uhn.fhir.rest.https.TrustStoreInfo; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.tls.KeyStoreInfo; +import ca.uhn.fhir.tls.TrustStoreInfo; import com.google.common.base.Charsets; import com.google.common.collect.Sets; import com.google.gson.JsonObject; diff --git a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulClientFactory.java b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulClientFactory.java index 63e93645aa3..1de55835dfe 100644 --- a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulClientFactory.java +++ b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulClientFactory.java @@ -5,9 +5,9 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.Header; import ca.uhn.fhir.rest.client.api.IHttpClient; import ca.uhn.fhir.rest.client.impl.RestfulClientFactory; -import ca.uhn.fhir.rest.https.TlsAuthentication; -import ca.uhn.fhir.rest.https.TlsAuthenticationSvc; -import ca.uhn.fhir.rest.https.TrustStoreInfo; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.rest.client.tls.TlsAuthenticationSvc; +import ca.uhn.fhir.tls.TrustStoreInfo; import okhttp3.Call; import okhttp3.OkHttpClient; diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactory.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactory.java index 6931a12dd9b..cc57121c130 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactory.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactory.java @@ -25,8 +25,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.Header; import ca.uhn.fhir.rest.client.api.IHttpClient; import ca.uhn.fhir.rest.client.impl.RestfulClientFactory; -import ca.uhn.fhir.rest.https.TlsAuthentication; -import ca.uhn.fhir.rest.https.TlsAuthenticationSvc; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.rest.client.tls.TlsAuthenticationSvc; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java index 067ba772c9d..18b4f41c4f0 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.i18n.Msg; import java.lang.reflect.*; import java.util.*; -import ca.uhn.fhir.rest.https.TlsAuthentication; +import ca.uhn.fhir.tls.TlsAuthentication; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthenticationSvc.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvc.java similarity index 95% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthenticationSvc.java rename to hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvc.java index 970a8c87ce8..c989b849691 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/https/TlsAuthenticationSvc.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvc.java @@ -1,6 +1,11 @@ -package ca.uhn.fhir.rest.https; +package ca.uhn.fhir.rest.client.tls; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.tls.KeyStoreInfo; +import ca.uhn.fhir.tls.PathType; +import ca.uhn.fhir.tls.StoreInfo; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.tls.TrustStoreInfo; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; diff --git a/hapi-fhir-client/src/test/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvcTest.java b/hapi-fhir-client/src/test/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvcTest.java new file mode 100644 index 00000000000..11de622b9fa --- /dev/null +++ b/hapi-fhir-client/src/test/java/ca/uhn/fhir/rest/client/tls/TlsAuthenticationSvcTest.java @@ -0,0 +1,110 @@ +package ca.uhn.fhir.rest.client.tls; + +import ca.uhn.fhir.tls.KeyStoreInfo; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.tls.TrustStoreInfo; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TlsAuthenticationSvcTest { + + private KeyStoreInfo myServerKeyStoreInfo; + private TrustStoreInfo myServerTrustStoreInfo; + private TlsAuthentication myServerTlsAuthentication; + + private KeyStoreInfo myClientKeyStoreInfo; + private TrustStoreInfo myClientTrustStoreInfo; + private TlsAuthentication myClientTlsAuthentication; + + @BeforeEach + public void beforeEach(){ + myServerKeyStoreInfo = new KeyStoreInfo("classpath:/server-keystore.p12", "changeit", "changeit", "server"); + myServerTrustStoreInfo = new TrustStoreInfo("classpath:/server-truststore.p12", "changeit", "client"); + myServerTlsAuthentication = new TlsAuthentication(Optional.of(myServerKeyStoreInfo), Optional.of(myServerTrustStoreInfo)); + + myClientKeyStoreInfo = new KeyStoreInfo("classpath:/client-keystore.p12", "changeit", "changeit", "client"); + myClientTrustStoreInfo = new TrustStoreInfo("classpath:/client-truststore.p12", "changeit", "server"); + myClientTlsAuthentication = new TlsAuthentication(Optional.of(myClientKeyStoreInfo), Optional.of(myClientTrustStoreInfo)); + } + + @Test + public void testCreateSslContextEmpty(){ + Optional emptyAuthentication = Optional.empty(); + Optional result = TlsAuthenticationSvc.createSslContext(emptyAuthentication); + assertTrue(result.isEmpty()); + } + + @Test + public void testCreateSslContextPresent(){ + Optional result = TlsAuthenticationSvc.createSslContext(Optional.of(myServerTlsAuthentication)); + assertFalse(result.isEmpty()); + assertEquals("TLS", result.get().getProtocol()); + } + + @Test + public void testCreateSslContextPresentInvalid(){ + KeyStoreInfo invalidKeyStoreInfo = new KeyStoreInfo("INVALID.p12", "changeit", "changeit", "server"); + TlsAuthentication invalidTlsAuthentication = new TlsAuthentication(Optional.of(invalidKeyStoreInfo), Optional.of(myServerTrustStoreInfo)); + assertThrows(TlsAuthenticationSvc.TlsAuthenticationException.class, () -> { + TlsAuthenticationSvc.createSslContext(Optional.of(invalidTlsAuthentication)); + }); + } + + @Test + public void testCreateKeyStore() throws Exception { + KeyStore keyStore = TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo); + assertNotNull(keyStore.getKey("server", myServerKeyStoreInfo.getKeyPass())); + } + + @Test + public void testCreateTrustStore() throws Exception { + KeyStore keyStore = TlsAuthenticationSvc.createKeyStore(myServerTrustStoreInfo); + assertNotNull(keyStore.getCertificate(myServerTrustStoreInfo.getAlias())); + } + + @Test + public void testCreateTrustManager() throws Exception{ + X509TrustManager trustManager = TlsAuthenticationSvc.createTrustManager(Optional.of(myClientTrustStoreInfo)); + KeyStore keyStore = TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo); + Certificate serverCertificate = keyStore.getCertificate(myServerKeyStoreInfo.getAlias()); + + assertEquals(1, trustManager.getAcceptedIssuers().length); + assertEquals(serverCertificate, trustManager.getAcceptedIssuers()[0]); + } + + @Test + public void testCreateTrustManagerInvalid() throws Exception{ + TrustStoreInfo invalidKeyStoreInfo = new TrustStoreInfo("INVALID.p12", "changeit", "client"); + X509TrustManager trustManager = TlsAuthenticationSvc.createTrustManager(Optional.of(invalidKeyStoreInfo)); + assertEquals(0, trustManager.getAcceptedIssuers().length); + } + + @Test + public void testCreateHostnameVerifierEmptyTrustStoreInfo(){ + Optional trustStoreInfo = Optional.empty(); + HostnameVerifier result = TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo); + assertEquals(NoopHostnameVerifier.class, result.getClass()); + } + + @Test + public void testCreateHostnameVerifierPresentTrustStoreInfo(){ + Optional trustStoreInfo = Optional.of(myServerTrustStoreInfo); + HostnameVerifier result = TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo); + assertEquals(DefaultHostnameVerifier.class, result.getClass()); + } +} diff --git a/hapi-fhir-client/src/test/resources/client-keystore.p12 b/hapi-fhir-client/src/test/resources/client-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c8ee5242febb69665825b06d3a3efffccc5f9b5c GIT binary patch literal 4339 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 Ylixyj7^TM)2`lKFV|9Ym<^lpI5JOi(6951J literal 0 HcmV?d00001 diff --git a/hapi-fhir-client/src/test/resources/server-keystore.p12 b/hapi-fhir-client/src/test/resources/server-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..00ae45348ef293d40db10cc642ca9796abf0d700 GIT binary patch literal 4408 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