Added upport for trust store when agent acts as web client 95/3695/3
authorPatrikBuhr <patrik.buhr@est.tech>
Thu, 7 May 2020 10:30:21 +0000 (12:30 +0200)
committerPatrikBuhr <patrik.buhr@est.tech>
Thu, 14 May 2020 11:58:31 +0000 (13:58 +0200)
Updated Dmaap so it uses http towards the agent NBI instead of https

Change-Id: Ia30ac783683efa478bc8ab56dc4ac8311b03f8f0
Issue-ID: NONRTRIC-195
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
12 files changed:
policy-agent/config/application.yaml
policy-agent/src/main/java/org/oransc/policyagent/BeanFactory.java
policy-agent/src/main/java/org/oransc/policyagent/clients/A1ClientFactory.java
policy-agent/src/main/java/org/oransc/policyagent/clients/AsyncRestClient.java
policy-agent/src/main/java/org/oransc/policyagent/clients/OscA1Client.java
policy-agent/src/main/java/org/oransc/policyagent/clients/StdA1ClientVersion1.java
policy-agent/src/main/java/org/oransc/policyagent/configuration/ApplicationConfig.java
policy-agent/src/main/java/org/oransc/policyagent/configuration/WebClientConfig.java [new file with mode: 0644]
policy-agent/src/main/java/org/oransc/policyagent/dmaap/DmaapMessageConsumer.java
policy-agent/src/main/resources/keystore.jks
policy-agent/src/test/java/org/oransc/policyagent/ApplicationTest.java
policy-agent/src/test/java/org/oransc/policyagent/MockPolicyAgent.java

index 4010219..55a1d64 100644 (file)
@@ -21,12 +21,17 @@ logging:
   file: /var/log/policy-agent/application.log
 server:
    port : 8433
+   http-port: 8081
    ssl:
-      key-store-type: PKCS12
+      key-store-type: JKS
       key-store-password: policy_agent
       key-store: classpath:keystore.jks
       key-password: policy_agent
+      key-alias: policy_agent
 app:
   filepath: /opt/app/policy-agent/config/application_configuration.json
-
+  webclient:
+    trust-store-used: false
+    trust-store-password: policy_agent
+    trust-store: classpath:keystore.jks
 
index e2874cb..1e01247 100644 (file)
@@ -29,6 +29,7 @@ import org.oransc.policyagent.repository.Policies;
 import org.oransc.policyagent.repository.PolicyTypes;
 import org.oransc.policyagent.repository.Rics;
 import org.oransc.policyagent.repository.Services;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
 import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
 import org.springframework.context.annotation.Bean;
@@ -38,6 +39,9 @@ import org.springframework.context.annotation.Configuration;
 class BeanFactory {
     private final ApplicationConfig applicationConfig = new ApplicationConfig();
 
+    @Value("${server.http-port}")
+    private int httpPort = 0;
+
     @Bean
     public Policies getPolicies() {
         return new Policies();
@@ -76,14 +80,16 @@ class BeanFactory {
     @Bean
     public ServletWebServerFactory servletContainer() {
         TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
-        tomcat.addAdditionalTomcatConnectors(getHttpConnector());
+        if (httpPort > 0) {
+            tomcat.addAdditionalTomcatConnectors(getHttpConnector(httpPort));
+        }
         return tomcat;
     }
 
-    private static Connector getHttpConnector() {
+    private static Connector getHttpConnector(int httpPort) {
         Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
         connector.setScheme("http");
-        connector.setPort(8081);
+        connector.setPort(httpPort);
         connector.setSecure(false);
         return connector;
     }
index 57ac980..322958a 100644 (file)
@@ -69,10 +69,10 @@ public class A1ClientFactory {
     A1Client createClient(Ric ric, A1ProtocolType version) throws ServiceException {
         if (version == A1ProtocolType.STD_V1_1) {
             assertNoControllerConfig(ric, version);
-            return new StdA1ClientVersion1(ric.getConfig());
+            return new StdA1ClientVersion1(ric.getConfig(), this.appConfig.getWebClientConfig());
         } else if (version == A1ProtocolType.OSC_V1) {
             assertNoControllerConfig(ric, version);
-            return new OscA1Client(ric.getConfig());
+            return new OscA1Client(ric.getConfig(), this.appConfig.getWebClientConfig());
         } else if (version == A1ProtocolType.SDNC_OSC_STD_V1_1 || version == A1ProtocolType.SDNC_OSC_OSC_V1) {
             return new SdncOscA1Client(version, ric.getConfig(), getControllerConfig(ric));
         } else if (version == A1ProtocolType.SDNC_ONAP) {
index 750b074..cefc7ca 100644 (file)
@@ -27,17 +27,29 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
 import io.netty.handler.timeout.ReadTimeoutHandler;
 import io.netty.handler.timeout.WriteTimeoutHandler;
 
+import java.io.FileInputStream;
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 
-import javax.net.ssl.SSLException;
-
+import org.oransc.policyagent.configuration.ImmutableWebClientConfig;
+import org.oransc.policyagent.configuration.WebClientConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.client.reactive.ReactorClientHttpConnector;
 import org.springframework.lang.Nullable;
+import org.springframework.util.ResourceUtils;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
 import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -54,9 +66,16 @@ public class AsyncRestClient {
     private WebClient webClient = null;
     private final String baseUrl;
     private static final AtomicInteger sequenceNumber = new AtomicInteger();
+    private final WebClientConfig clientConfig;
 
     public AsyncRestClient(String baseUrl) {
+        this(baseUrl,
+            ImmutableWebClientConfig.builder().isTrustStoreUsed(false).trustStore("").trustStorePassword("").build());
+    }
+
+    public AsyncRestClient(String baseUrl, WebClientConfig config) {
         this.baseUrl = baseUrl;
+        this.clientConfig = config;
     }
 
     public Mono<ResponseEntity<String>> postForEntity(String uri, @Nullable String body) {
@@ -185,13 +204,53 @@ public class AsyncRestClient {
         }
     }
 
-    private static SslContext createSslContext() throws SSLException {
+    private boolean isCertificateEntry(KeyStore trustStore, String alias) {
+        try {
+            return trustStore.isCertificateEntry(alias);
+        } catch (KeyStoreException e) {
+            logger.error("Error reading truststore {}", e.getMessage());
+            return false;
+        }
+    }
+
+    private Certificate getCertificate(KeyStore trustStore, String alias) {
+        try {
+            return trustStore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error("Error reading truststore {}", e.getMessage());
+            return null;
+        }
+    }
+
+    SslContext createSslContextSecure(String trustStorePath, String trustStorePass)
+        throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
+
+        final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
+
+        List<Certificate> certificateList = Collections.list(trustStore.aliases()).stream() //
+            .filter(alias -> isCertificateEntry(trustStore, alias)) //
+            .map(alias -> getCertificate(trustStore, alias)) //
+            .collect(Collectors.toList());
+        final X509Certificate[] certificates = certificateList.toArray(new X509Certificate[certificateList.size()]);
+
         return SslContextBuilder.forClient() //
-            .trustManager(InsecureTrustManagerFactory.INSTANCE) //
+            .trustManager(certificates) //
             .build();
     }
 
-    private static WebClient createWebClient(String baseUrl, SslContext sslContext) {
+    private SslContext createSslContext()
+        throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
+        if (this.clientConfig.isTrustStoreUsed()) {
+            return createSslContextSecure(this.clientConfig.trustStore(), this.clientConfig.trustStorePassword());
+        } else {
+            return SslContextBuilder.forClient() //
+                .trustManager(InsecureTrustManagerFactory.INSTANCE) //
+                .build();
+        }
+    }
+
+    private WebClient createWebClient(String baseUrl, SslContext sslContext) {
         TcpClient tcpClient = TcpClient.create() //
             .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) //
             .secure(c -> c.sslContext(sslContext)) //
@@ -213,7 +272,7 @@ public class AsyncRestClient {
             try {
                 SslContext sslContext = createSslContext();
                 this.webClient = createWebClient(this.baseUrl, sslContext);
-            } catch (SSLException e) {
+            } catch (Exception e) {
                 logger.error("Could not create WebClient {}", e.getMessage());
                 return Mono.error(e);
             }
index 99e5bae..a388267 100644 (file)
@@ -25,6 +25,7 @@ import java.util.List;
 
 import org.json.JSONObject;
 import org.oransc.policyagent.configuration.RicConfig;
+import org.oransc.policyagent.configuration.WebClientConfig;
 import org.oransc.policyagent.repository.Policy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -116,8 +117,8 @@ public class OscA1Client implements A1Client {
     private final AsyncRestClient restClient;
     private final UriBuilder uri;
 
-    public OscA1Client(RicConfig ricConfig) {
-        this(ricConfig, new AsyncRestClient(""));
+    public OscA1Client(RicConfig ricConfig, WebClientConfig clientConfig) {
+        this(ricConfig, new AsyncRestClient("", clientConfig));
     }
 
     public OscA1Client(RicConfig ricConfig, AsyncRestClient restClient) {
index f5486a5..4ebc25c 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Arrays;
 import java.util.List;
 
 import org.oransc.policyagent.configuration.RicConfig;
+import org.oransc.policyagent.configuration.WebClientConfig;
 import org.oransc.policyagent.repository.Policy;
 
 import reactor.core.publisher.Flux;
@@ -84,8 +85,8 @@ public class StdA1ClientVersion1 implements A1Client {
     private final AsyncRestClient restClient;
     private final UriBuilder uri;
 
-    public StdA1ClientVersion1(RicConfig ricConfig) {
-        this(new AsyncRestClient(""), ricConfig);
+    public StdA1ClientVersion1(RicConfig ricConfig, WebClientConfig webClientConfig) {
+        this(new AsyncRestClient("", webClientConfig), ricConfig);
     }
 
     public StdA1ClientVersion1(AsyncRestClient restClient, RicConfig ricConfig) {
index 052a96c..6c2c91b 100644 (file)
@@ -31,15 +31,27 @@ import javax.validation.constraints.NotEmpty;
 import lombok.Getter;
 
 import org.oransc.policyagent.exceptions.ServiceException;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import reactor.core.publisher.Flux;
 
 @EnableConfigurationProperties
-@ConfigurationProperties("app")
+@ConfigurationProperties()
 public class ApplicationConfig {
     @NotEmpty
-    private String filepath;
+    @Getter
+    @Value("${app.filepath}")
+    private String localConfigurationFilePath;
+
+    @Value("${app.webclient.trust-store-used}")
+    private boolean sslTrustStoreUsed = false;
+
+    @Value("${app.webclient.trust-store-password}")
+    private String sslTrustStorePassword = "";
+
+    @Value("${app.webclient.trust-store}")
+    private String sslTrustStore = "";
 
     private Map<String, RicConfig> ricConfigs = new HashMap<>();
     @Getter
@@ -49,21 +61,18 @@ public class ApplicationConfig {
 
     private Map<String, ControllerConfig> controllerConfigs = new HashMap<>();
 
-    public String getLocalConfigurationFilePath() {
-        return this.filepath;
-    }
-
-    /*
-     * Do not remove, used by framework!
-     */
-    public synchronized void setFilepath(String filepath) {
-        this.filepath = filepath;
-    }
-
     public synchronized Collection<RicConfig> getRicConfigs() {
         return this.ricConfigs.values();
     }
 
+    public WebClientConfig getWebClientConfig() {
+        return ImmutableWebClientConfig.builder() //
+            .isTrustStoreUsed(this.sslTrustStoreUsed) //
+            .trustStore(this.sslTrustStore) //
+            .trustStorePassword(this.sslTrustStorePassword) //
+            .build();
+    }
+
     public synchronized ControllerConfig getControllerConfig(String name) throws ServiceException {
         ControllerConfig controllerConfig = this.controllerConfigs.get(name);
         if (controllerConfig == null) {
diff --git a/policy-agent/src/main/java/org/oransc/policyagent/configuration/WebClientConfig.java b/policy-agent/src/main/java/org/oransc/policyagent/configuration/WebClientConfig.java
new file mode 100644 (file)
index 0000000..5f49498
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 Nordix Foundation
+ * %%
+ * 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.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.policyagent.configuration;
+
+import org.immutables.value.Value;
+
+@Value.Immutable
+@Value.Style(redactedMask = "####")
+public interface WebClientConfig {
+    public boolean isTrustStoreUsed();
+
+    @Value.Redacted
+    public String trustStorePassword();
+
+    public String trustStore();
+
+}
index f13ffeb..011b977 100644 (file)
@@ -64,8 +64,8 @@ public class DmaapMessageConsumer {
     private DmaapMessageHandler dmaapMessageHandler = null;
     private MRConsumer messageRouterConsumer = null;
 
-    @Value("${server.port}")
-    private int localServerPort;
+    @Value("${server.http-port}")
+    private int localServerHttpPort;
 
     @Autowired
     public DmaapMessageConsumer(ApplicationConfig applicationConfig) {
@@ -139,7 +139,7 @@ public class DmaapMessageConsumer {
 
     protected DmaapMessageHandler getDmaapMessageHandler() throws IOException {
         if (this.dmaapMessageHandler == null) {
-            String agentBaseUrl = "https://localhost:" + this.localServerPort;
+            String agentBaseUrl = "http://localhost:" + this.localServerHttpPort;
             AsyncRestClient agentClient = new AsyncRestClient(agentBaseUrl);
             Properties dmaapPublisherProperties = applicationConfig.getDmaapPublisherConfig();
             MRBatchingPublisher producer = MRClientFactory.createBatchingPublisher(dmaapPublisherProperties);
index 3cd6bb7..4df793d 100644 (file)
Binary files a/policy-agent/src/main/resources/keystore.jks and b/policy-agent/src/main/resources/keystore.jks differ
index 0fd4a33..0966257 100644 (file)
@@ -109,6 +109,9 @@ public class ApplicationTest {
     @Autowired
     RicSupervision supervision;
 
+    @Autowired
+    ApplicationConfig applicationConfig;
+
     @Autowired
     Services services;
 
@@ -723,7 +726,7 @@ public class ApplicationTest {
     }
 
     private AsyncRestClient restClient() {
-        return new AsyncRestClient(baseUrl());
+        return new AsyncRestClient(baseUrl(), this.applicationConfig.getWebClientConfig());
     }
 
     private void testErrorCode(Mono<?> request, HttpStatus expStatus) {
index 1636418..7f57ceb 100644 (file)
@@ -67,6 +67,9 @@ public class MockPolicyAgent {
     @Autowired
     PolicyTypes policyTypes;
 
+    @Autowired
+    ApplicationConfig applicationConfig;
+
     static class MockApplicationConfig extends ApplicationConfig {
         @Override
         public String getLocalConfigurationFilePath() {
@@ -195,7 +198,8 @@ public class MockPolicyAgent {
     }
 
     @Test
-    @SuppressWarnings("squid:S2699") // Tests should include assertions. This test is only for keeping the server alive,
+    @SuppressWarnings("squid:S2699") // Tests should include assertions. This test is only for keeping the server
+                                     // alive,
                                      // so it will only be confusing to add an assertion.
     public void runMock() throws Exception {
         keepServerAlive();