You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by jf...@apache.org on 2020/03/11 16:15:46 UTC

[plc4x] 04/06: [OPCUA] Finished work on Certificate integartion.

This is an automated email from the ASF dual-hosted git repository.

jfeinauer pushed a commit to branch rel/0.6
in repository https://gitbox.apache.org/repos/asf/plc4x.git

commit 1b549ba818ef6cab5048d6a5d9e98f221bdaf57d
Author: Julian Feinauer <j....@pragmaticminds.de>
AuthorDate: Tue Mar 10 22:32:11 2020 +0100

    [OPCUA] Finished work on Certificate integartion.
---
 plc4j/drivers/opcua/pom.xml                        | 54 ++++++-------
 .../apache/plc4x/java/opcua/OpcuaPlcDriver.java    |  2 +-
 .../opcua/connection/BaseOpcuaPlcConnection.java   | 31 ++++++-
 .../java/opcua/connection/KeyStoreLoader.java      | 94 ++++++++++++++++++++--
 .../opcua/connection/OpcuaTcpPlcConnection.java    | 57 +++++++++----
 5 files changed, 181 insertions(+), 57 deletions(-)

diff --git a/plc4j/drivers/opcua/pom.xml b/plc4j/drivers/opcua/pom.xml
index 7150836..54e87c3 100644
--- a/plc4j/drivers/opcua/pom.xml
+++ b/plc4j/drivers/opcua/pom.xml
@@ -17,7 +17,8 @@
   specific language governing permissions and limitations
   under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
   <modelVersion>4.0.0</modelVersion>
 
@@ -60,23 +61,16 @@
       <version>0.3.6</version>
     </dependency>
     <dependency>
-      <groupId>org.eclipse.milo</groupId>
-      <artifactId>server-examples</artifactId>
-      <version>0.3.6</version>
-    </dependency>
-
-    <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
     </dependency>
 
-
-  <dependency>
-    <groupId>org.osgi</groupId>
-    <artifactId>osgi.cmpn</artifactId>
-    <version>6.0.0</version>
-    <scope>provided</scope>
-  </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>osgi.cmpn</artifactId>
+      <version>6.0.0</version>
+      <scope>provided</scope>
+    </dependency>
 
     <dependency>
       <groupId>org.bouncycastle</groupId>
@@ -89,22 +83,22 @@
       <version>${bouncycastle.version}</version>
     </dependency>
 
-</dependencies>
+  </dependencies>
 
 
-<!--
-<build>
-  <plugins>
-    <plugin>
-      <groupId>org.apache.maven.plugins</groupId>
-      <artifactId>maven-dependency-plugin</artifactId>
-      <configuration>
-        <usedDependencies combine.children="append">
-          <usedDependency>org.eclipse.milo:sdk-client</usedDependency>
-        </usedDependencies>
-      </configuration>
-    </plugin>
-  </plugins>
-</build>
--->
+  <!--
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <configuration>
+          <usedDependencies combine.children="append">
+            <usedDependency>org.eclipse.milo:sdk-client</usedDependency>
+          </usedDependencies>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  -->
 </project>
\ No newline at end of file
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
index 9f3de51..54e51dd 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
@@ -47,7 +47,7 @@ public class OpcuaPlcDriver implements PlcDriver {
 
     public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("tcp://(?<host>[\\w.-]+)(:(?<port>\\d*))?");
     public static final Pattern OPCUA_URI_PARAM_PATTERN = Pattern.compile("(?<param>[(\\?|\\&)([^=]+)\\=([^&]+)]+)?"); //later used for regex filtering of the params
-    public static final Pattern OPCUA_URI_PATTERN = Pattern.compile("^opcua:(" + INET_ADDRESS_PATTERN + ")?" + "(?<params>[\\w/=?&]+)?");
+    public static final Pattern OPCUA_URI_PATTERN = Pattern.compile("^opcua:(" + INET_ADDRESS_PATTERN + ")?" + "(?<params>[\\w/=?&.\\d*-_]+)?");
     private static final int requestTimeout = 10000;
     private OpcuaConnectionFactory opcuaConnectionFactory;
 
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java
index ac6fba4..5ca6783 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java
@@ -26,6 +26,7 @@ import org.apache.plc4x.java.api.messages.PlcWriteRequest;
 import org.apache.plc4x.java.base.connection.AbstractPlcConnection;
 import org.apache.plc4x.java.base.messages.*;
 import org.apache.plc4x.java.opcua.protocol.OpcuaPlcFieldHandler;
+import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,13 +39,15 @@ public abstract class BaseOpcuaPlcConnection extends AbstractPlcConnection imple
     protected boolean skipDiscovery = false;
     protected String username = null;
     protected String password = null;
-    protected String certFile;
+    protected String certFile = null;
+    protected SecurityPolicy securityPolicy = null;
+    protected String keyStoreFile = null;
+//    protected String keyStorePassword;
 
     /**
      * @param params
      */
     BaseOpcuaPlcConnection(String params) {
-
         if (!StringUtils.isEmpty(params)) {
             for (String param : params.split("&")) {
                 String[] paramElements = param.split("=");
@@ -53,16 +56,38 @@ public abstract class BaseOpcuaPlcConnection extends AbstractPlcConnection imple
                     String paramValue = paramElements[1];
                     switch (paramName) {
                         case "discovery":
-                            skipDiscovery = !Boolean.valueOf(paramValue);
+                            skipDiscovery = !Boolean.parseBoolean(paramValue);
+                            logger.debug("Found Parameter 'skipDiscovery' with value {}", this.skipDiscovery);
                             break;
                         case "username":
                             username = paramValue;
+                            logger.debug("Found Parameter 'username' with value {}", username);
                             break;
                         case "password":
                             password = paramValue;
+                            logger.debug("Found Parameter 'password' with value {}", password);
                             break;
                         case "certFile":
                             certFile = paramValue;
+                            logger.debug("Found Parameter 'certFile' with value {}", certFile);
+                            break;
+                        case "securityPolicy":
+                            logger.debug("Got value for security policy: '{}', trying to parse", paramValue);
+                            try {
+                                securityPolicy = SecurityPolicy.valueOf(paramValue);
+                                logger.debug("Using Security Policy {}", securityPolicy);
+                            } catch (IllegalArgumentException e) {
+                                logger.warn("Unable to parse policy {}", paramValue);
+                            }
+                            break;
+                        case "keyStoreFile":
+                            keyStoreFile = paramValue;
+                            logger.debug("Found Parameter 'keyStoreFile' with value {}", keyStoreFile);
+                            break;
+//                        case "keyStorePassword":
+//                            keyStorePassword = paramValue;
+//                            logger.debug("Found Parameter 'keyStorePassword' with value {}", keyStorePassword);
+//                            break;
                         default:
                             logger.debug("Unknown parameter {} with value {}", paramName, paramValue);
                     }
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/KeyStoreLoader.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/KeyStoreLoader.java
index 821f58d..abd2d48 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/KeyStoreLoader.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/KeyStoreLoader.java
@@ -1,6 +1,6 @@
 package org.apache.plc4x.java.opcua.connection;
 
-import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
+import com.google.common.collect.Sets;
 import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
 import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
 import org.slf4j.Logger;
@@ -8,6 +8,11 @@ import org.slf4j.LoggerFactory;
 
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.Key;
@@ -16,6 +21,11 @@ import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 /**
@@ -27,18 +37,47 @@ public class KeyStoreLoader {
     private static final Pattern IP_ADDR_PATTERN = Pattern.compile(
         "^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
 
-    private static final String CLIENT_ALIAS = "client-ai";
-    private static final char[] PASSWORD = "password".toCharArray();
+    private static final String CLIENT_ALIAS = "client-cert";
+    private static final char[] PASSWORD = "plc4x".toCharArray();
 
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
     private X509Certificate clientCertificate;
     private KeyPair clientKeyPair;
 
+//    KeyStoreLoader load(Path baseDir) throws Exception {
+//        return load(baseDir, true);
+//    }
+//
+//    KeyStoreLoader load(Path baseDir, boolean resolve) throws Exception {
+//        return load(baseDir, null, resolve);
+//    }
+//
+//    KeyStoreLoader load(Path file, String password) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
+//        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+//
+//        Path serverKeyStore = file;
+//
+//        logger.info("Loading KeyStore at {}", serverKeyStore);
+//
+//        try (InputStream in = Files.newInputStream(serverKeyStore)) {
+//            keyStore.load(in, password.toCharArray());
+//        }
+//
+//        Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, password.toCharArray());
+//        if (serverPrivateKey instanceof PrivateKey) {
+//            clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
+//            PublicKey serverPublicKey = clientCertificate.getPublicKey();
+//            clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);
+//        }
+//
+//        return this;
+//    }
+
     KeyStoreLoader load(Path baseDir) throws Exception {
         KeyStore keyStore = KeyStore.getInstance("PKCS12");
 
-        Path serverKeyStore = baseDir.resolve("example-client.pfx");
+        Path serverKeyStore = baseDir;
 
         logger.info("Loading KeyStore at {}", serverKeyStore);
 
@@ -54,12 +93,13 @@ public class KeyStoreLoader {
                 .setLocalityName("Folsom")
                 .setStateName("CA")
                 .setCountryCode("US")
-                .setApplicationUri("urn:eclipse:milo:examples:client")
+//                .setApplicationUri("urn:eclipse:milo:examples:client")
+                .setApplicationUri("urn:plc4x-client")
                 .addDnsName("localhost")
                 .addIpAddress("127.0.0.1");
 
             // Get as many hostnames and IP addresses as we can listed in the certificate.
-            for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
+            for (String hostname : getHostnames("0.0.0.0", true)) {
                 if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
                     builder.addIpAddress(hostname);
                 } else {
@@ -97,4 +137,46 @@ public class KeyStoreLoader {
         return clientKeyPair;
     }
 
+    public Set<String> getHostnames(String address, boolean includeLoopback) {
+        HashSet hostnames = Sets.newHashSet();
+
+        try {
+            InetAddress inetAddress = InetAddress.getByName(address);
+            if (inetAddress.isAnyLocalAddress()) {
+                try {
+                    Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
+                    Iterator var5 = Collections.list(nis).iterator();
+
+                    while(var5.hasNext()) {
+                        NetworkInterface ni = (NetworkInterface)var5.next();
+                        Collections.list(ni.getInetAddresses()).forEach((ia) -> {
+                            if (ia instanceof Inet4Address) {
+                                boolean loopback = ia.isLoopbackAddress();
+                                if (!loopback || includeLoopback) {
+                                    hostnames.add(ia.getHostName());
+                                    hostnames.add(ia.getHostAddress());
+                                    hostnames.add(ia.getCanonicalHostName());
+                                }
+                            }
+
+                        });
+                    }
+                } catch (SocketException var7) {
+                    logger.warn("Failed to NetworkInterfaces for bind address: {}", address, var7);
+                }
+            } else {
+                boolean loopback = inetAddress.isLoopbackAddress();
+                if (!loopback || includeLoopback) {
+                    hostnames.add(inetAddress.getHostName());
+                    hostnames.add(inetAddress.getHostAddress());
+                    hostnames.add(inetAddress.getCanonicalHostName());
+                }
+            }
+        } catch (UnknownHostException var8) {
+            logger.warn("Failed to get InetAddress for bind address: {}", address, var8);
+        }
+
+        return hostnames;
+    }
+
 }
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java
index 76db3c0..d1bbf62 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java
@@ -198,11 +198,14 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
             }
 
         }
+
         endpoint = endpoints.stream()
             .filter(e -> e.getSecurityPolicyUri().equals(getSecurityPolicy().getUri()))
             .filter(endpointFilter())
             .findFirst()
-            .orElseThrow(() -> new PlcConnectionException("No desired endpoints from"));
+            .orElseThrow(() -> new PlcConnectionException("No desired endpoints found (with right policy)"));
+
+        logger.debug("Using Endpoint {}", endpoint);
 
         // Security
         Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
@@ -219,15 +222,20 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
 
         KeyStoreLoader loader;
         try {
-            loader = new KeyStoreLoader().load(securityTempDir);
+            if (this.keyStoreFile == null) {
+                logger.debug("No KeyStoreFile given, generating...");
+                loader = new KeyStoreLoader().load(securityTempDir.resolve("client-cert.pfx"));
+            } else {
+                logger.debug("KeyStoreFile {} given, loading or generating there...", this.keyStoreFile);
+                loader = new KeyStoreLoader().load(Paths.get(keyStoreFile));
+            }
         } catch (Exception e) {
             throw new PlcConnectionException("Unable to load Security!", e);
         }
 
-        SecurityPolicy securityPolicy = SecurityPolicy.None;
-
+        // Make Security Configurable
         logger.info("Using endpoint: {} [{}/{}]",
-            endpoint.getEndpointUrl(), securityPolicy, endpoint.getSecurityMode());
+            endpoint.getEndpointUrl(), getSecurityPolicy(), endpoint.getSecurityMode());
         // End Security
 
         if (this.skipDiscovery) {
@@ -277,8 +285,9 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
 
         OpcUaClientConfig config = OpcUaClientConfig.builder()
             .setApplicationName(LocalizedText.english("eclipse milo opc-ua client of the apache PLC4X:PLC4J project"))
-            .setApplicationUri("urn:eclipse:milo:plc4x:client")
+            .setApplicationUri("urn:plc4x-client")
             .setCertificate(loader.getClientCertificate())
+            .setKeyPair(loader.getClientKeyPair())
             .setEndpoint(endpoint)
             .setIdentityProvider(getIdentityProvider())
             .setRequestTimeout(UInteger.valueOf(requestTimeout))
@@ -298,6 +307,22 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
         }
     }
 
+//    private X509Certificate getClientCertificate(KeyStoreLoader loader) {
+//        if (this.certFile != null) {
+//            Path path = null;
+//            try {
+//                path = Paths.get(this.certFile);
+//                final X509Certificate x509Certificate = CertificateUtil.decodeCertificate(Files.readAllBytes(path));
+//                logger.info("Using Certificate given by certFile as Client Certificate");
+//                return x509Certificate;
+//            } catch (UaException | IOException e) {
+//                logger.warn("Unable to load given Certificate File {}", path != null ? path.toAbsolutePath().toString() : this.certFile, e);
+//            }
+//        }
+//        logger.info("Using self signed generated Client Certificate");
+//        return loader.getClientCertificate();
+//    }
+
     @Override
     public boolean isConnected() {
         return client != null && isConnected;
@@ -545,7 +570,13 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
     }
 
     private SecurityPolicy getSecurityPolicy() {
-        return SecurityPolicy.None;
+        if (this.securityPolicy == null) {
+            logger.debug("No Security Policy given, using default NONE");
+            return SecurityPolicy.None;
+        } else {
+            logger.debug("Using given SecurityPolicy {}", this.securityPolicy);
+            return this.securityPolicy;
+        }
     }
 
     private IdentityProvider getIdentityProvider() {
@@ -553,18 +584,10 @@ public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
             if (this.password == null) {
                 throw new PlcRuntimeException("Username given, but no password. Please Add password by providing &password=");
             }
-            logger.info("Username {} is given and password {}, using Basic Auth now", this.username, this.password);
+            logger.debug("Username {} is given and password {}, using Basic Auth now", this.username, this.password);
             return new UsernameProvider(this.username, this.password);
-//        } else if (this.certFile != null) {
-//            logger.info("Using Certificate Path {}", this.certFile);
-//            try {
-//                final X509Certificate x509Certificate = CertificateUtil.decodeCertificate(Files.readAllBytes(new File(this.certFile).toPath()));
-//                return new X509IdentityProvider(x509Certificate);
-//            } catch (UaException | IOException e) {
-//                throw new PlcRuntimeException("Unable to load or decode Cert with path " + this.certFile);
-//            }
         } else {
-            logger.info("No username / password is given, using anonymous access");
+            logger.debug("No username / password is given, using anonymous access");
             return new AnonymousProvider();
         }
     }