diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java index 3df8ed6d31a..4fdaee87967 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java @@ -24,22 +24,19 @@ public class Limits implements Configuration { - private static final int DEFAULT_MAX_CHUNK_COUNT = 64; - private static final int DEFAULT_MAX_MESSAGE_SIZE = 2097152; private static final int DEFAULT_RECEIVE_BUFFER_SIZE = 65535; private static final int DEFAULT_SEND_BUFFER_SIZE = 65535; + private static final int DEFAULT_MAX_MESSAGE_SIZE = 2097152; + private static final int DEFAULT_MAX_CHUNK_COUNT = 64; - public static final Limits DEFAULT = new Limits( - DEFAULT_RECEIVE_BUFFER_SIZE, - DEFAULT_SEND_BUFFER_SIZE, - DEFAULT_MAX_MESSAGE_SIZE, - DEFAULT_MAX_CHUNK_COUNT - ); - - private final int receiveBufferSize; - private final int sendBufferSize; - private final int maxMessageSize; - private final int maxChunkCount; + @ConfigurationParameter("receiveBufferSize") + private int receiveBufferSize; + @ConfigurationParameter("sendBufferSize") + private int sendBufferSize; + @ConfigurationParameter("maxMessageSize") + private int maxMessageSize; + @ConfigurationParameter("maxChunkCount") + private int maxChunkCount; public Limits() { this(DEFAULT_RECEIVE_BUFFER_SIZE, DEFAULT_SEND_BUFFER_SIZE, DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_MAX_CHUNK_COUNT); diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java index 424b7a6f9c3..16b813c5ae8 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java @@ -19,9 +19,6 @@ package org.apache.plc4x.java.opcua.config; import java.security.cert.X509Certificate; -import java.time.Duration; -import org.apache.plc4x.java.opcua.context.SecureChannel; -import org.apache.plc4x.java.opcua.readwrite.PascalByteString; import org.apache.plc4x.java.opcua.security.MessageSecurity; import org.apache.plc4x.java.opcua.security.SecurityPolicy; import org.apache.plc4x.java.spi.configuration.Configuration; @@ -31,11 +28,12 @@ public class OpcuaConfiguration implements Configuration { - public static final long DEFAULT_CONNECTION_LIFETIME = 36000000; + public static final long DEFAULT_CHANNEL_LIFETIME = 3600000; - public static final long DEFAULT_REQUEST_TIMEOUT = 5 * 60 * 1000L; + public static final long DEFAULT_SESSION_TIMEOUT = 120000; + public static final long DEFAULT_NEGOTIATION_TIMEOUT = 60000; - public static final long DEFAULT_SESSION_TIMEOUT = 120000L; + public static final long DEFAULT_REQUEST_TIMEOUT = 30000; @ConfigurationParameter("protocolCode") private String protocolCode; @@ -73,16 +71,16 @@ public class OpcuaConfiguration implements Configuration { private X509Certificate serverCertificate; @ConfigurationParameter("channelLifetime") - private long channelLifetime = DEFAULT_CONNECTION_LIFETIME; - - @ConfigurationParameter("requestTimeout") - private long requestTimeout = DEFAULT_REQUEST_TIMEOUT; + private long channelLifetime = DEFAULT_CHANNEL_LIFETIME; @ConfigurationParameter("sessionTimeout") private long sessionTimeout = DEFAULT_SESSION_TIMEOUT; - @ConfigurationParameter("openChannelTimeout") - private long openChannelTimeout = DEFAULT_REQUEST_TIMEOUT; + @ConfigurationParameter("negotiationTimeout") + private long negotiationTimeout = DEFAULT_NEGOTIATION_TIMEOUT; + + @ConfigurationParameter("requestTimeout") + private long requestTimeout = DEFAULT_REQUEST_TIMEOUT; @ComplexConfigurationParameter(prefix = "encoding", defaultOverrides = {}, requiredOverrides = {}) private Limits limits = new Limits(); @@ -147,12 +145,16 @@ public long getChannelLifetime() { return channelLifetime; } + public long getSessionTimeout() { + return sessionTimeout; + } + public long getRequestTimeout() { return requestTimeout; } - public long getOpenChannelTimeout() { - return openChannelTimeout; + public long getNegotiationTimeout() { + return negotiationTimeout; } @Override diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java index 94df2cdbcc8..d398d76951e 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java @@ -179,7 +179,7 @@ public CompletableFuture requestHello() { //CompletableFuture future = new CompletableFuture<>(); CompletableFuture future = new CompletableFuture<>(); - sendRequest(request, future, configuration.getOpenChannelTimeout()) + sendRequest(request, future, configuration.getNegotiationTimeout()) .unwrap(OpcuaAPU::getMessage) .check(OpcuaAcknowledgeResponse.class::isInstance) .unwrap(OpcuaAcknowledgeResponse.class::cast) @@ -239,7 +239,7 @@ private CompletableFuture reques for (int count = chunks.size(), index = 0; index < count; index++) { boolean last = index + 1 == count; if (last) { - sendRequest(chunks.get(index), future, configuration.getRequestTimeout()) + sendRequest(chunks.get(index), future, configuration.getNegotiationTimeout()) .unwrap(OpcuaAPU::getMessage) .check(replyType::isInstance) .unwrap(replyType::cast) diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java index 7c6a36025fb..7efbb821157 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java @@ -54,7 +54,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -89,7 +88,7 @@ public class SecureChannel { private final RequestTransactionManager tm; private final OpcuaConfiguration configuration; private final OpcuaDriverContext driverContext; - private Conversation conversation; + private final Conversation conversation; private ScheduledFuture keepAlive; private final List endpoints = new ArrayList<>(); private double sessionTimeout; @@ -101,6 +100,7 @@ public SecureChannel(Conversation conversation, RequestTransactionManager tm, Op this.configuration = configuration; this.driverContext = driverContext; this.endpoint = new PascalString(driverContext.getEndpoint()); + this.sessionTimeout = configuration.getSessionTimeout(); if (authentication != null) { if (authentication instanceof PlcUsernamePasswordAuthentication) { this.username = ((PlcUsernamePasswordAuthentication) authentication).getUsername(); @@ -155,7 +155,7 @@ public CompletableFuture onConnect() { public CompletableFuture onConnectOpenSecureChannel(SecurityTokenRequestType securityTokenRequestType) { LOGGER.debug("Sending open secure channel message to {}", this.driverContext.getEndpoint()); - RequestHeader requestHeader = conversation.createRequestHeader(configuration.getOpenChannelTimeout(), 0); + RequestHeader requestHeader = conversation.createRequestHeader(configuration.getNegotiationTimeout(), 0); OpenSecureChannelRequest openSecureChannelRequest; if (conversation.getSecurityPolicy() != SecurityPolicy.NONE) { diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java index 074b264bab1..1d514703fe5 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java @@ -45,7 +45,6 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; diff --git a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java index 58e77fa7237..39258661406 100644 --- a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java +++ b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java @@ -19,7 +19,6 @@ package org.eclipse.milo.examples.server; -import static com.google.common.collect.Lists.newArrayList; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_X509; @@ -165,7 +164,7 @@ public TestMiloServer() throws Exception { private Set createEndpointConfigurations(X509Certificate certificate) { Set endpointConfigurations = new LinkedHashSet<>(); - List bindAddresses = newArrayList(); + List bindAddresses = new ArrayList<>(); bindAddresses.add("0.0.0.0"); Set hostnames = new LinkedHashSet<>(); diff --git a/src/site/asciidoc/users/protocols/opc-ua.adoc b/src/site/asciidoc/users/protocols/opc-ua.adoc index f45be40c4d2..62906fc0479 100644 --- a/src/site/asciidoc/users/protocols/opc-ua.adoc +++ b/src/site/asciidoc/users/protocols/opc-ua.adoc @@ -64,8 +64,35 @@ will propagate over an '
/discovery' endpoint. The most common issue her configured and propagate the wrong external IP or URL address. If that is the case you can disable the discovery by configuring it with a `false` value. -|| `username` | A username to authenticate to the OPCUA server with. -|| `password` | A password to authenticate to the OPCUA server with. | +The discovery phase is always conducted using `NONE` security policy. + +| `securityPolicy` | `NONE` | The security policy applied to communication channel between driver and OPC UA server. +Default value assumes. Possible options are `NONE`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss`. +| `messageSecurity` | `SIGN_ENCRYPT` | The security policy applied to messages exchanged after handshake phase. +Each handshake uses `SIGN_ENCRYPT` mode to securely negotiate encryption keys used later for symmetric encryption. +In case when security policy defines secure value (i.e. `Basic256`) it will automatically impose full security on the message exchanges. + +| `certDirectory` | | Filesystem location where security related files are expected to be found. +| `keyStoreFile` | | The Keystore file used to lookup client certificate and its private key. +Please note that in case if no value is provided driver will generate self-signed certificate for negotiation phase. +The keystore file is looked up under `$certDirectory/security/` directory. +Expected keystore type is PKCS12 (default since Java 11). +| `keyStorePassword` | | Java keystore password used to access keystore and private key. + +| `username` | | A username to authenticate to the OPCUA server with. +| `password` | | A password to authenticate to the OPCUA server with. + +3+| TCP encoding options +| `encoding.receiveBufferSize` | 65535 | Maximum size of received TCP transport message chunk value in bytes. +| `encoding.sendBufferSize` | 65535 | Maximum size of sent transport message chunk. +| `encoding.maxMessageSize` | 2097152 | Maximum size of complete message. +| `encoding.maxChunkCount` | 64 | Maximum number of chunks for both sent and received messages. + +3+| Timeout options +| `channelLifetime` | `3600000` | Time for which negotiated secure channel, its keys and session remains open. Value in milliseconds, by default 60 minutes. +| `sessionTimeout` | `120000` | Expiry time for opened secure session, value in milliseconds. Defaults to 2 minutes. +| `negotiationTimeout` | `60000` | Timeout for all negotiation steps prior acceptance of application level operations - this timeout applies to open secure channel, create session and close calls. Defaults to 60 seconds. +| `requestTimeout` | `30000` | Timeout for read/write/subscribe calls. Value in milliseconds. |=== @@ -89,6 +116,32 @@ opcua:tcp://127.0.0.1:12686?discovery=true&username=admin&password=password Note the transport, port and options fields are optional. +=== Secure communication +The secure channel implementation within Apache PLC4X project have been tested against existing open source server implementations. +This includes Eclipse Milo (all modes) as well as OPC Foundation .NET server (except `Basic128Rsa15`). +Manual tests proven that driver is able to communicate with OPC UA server launched on PLCs as well as commercial simulators. + +Depending on actual configuration of remote end there might be necessity to prepare client certificate. +Preparation of certificate is beyond driver, however in case when no client certificate is provided, it will be auto-generated to establish a session. + +The security modes differ between themselves by strength of applied signature and encryption algorithms. +Driver is able to communicate with single security mode at the time. +Additionally, to security policy it is possible to specify `messageSecurity` option which indicates expected security settings after initial handshake. +By default, this option is set to `SIGN_ENCRYPT` which imposes high security settings and full encryption of exchanged message payloads. +In case when additional diagnostics is needed payloads has to be traced through TRACE level log entries. +The `SIGN` mode gives possibility o browse packets in tools such wireshark. + +==== Negotiation procedure +Depending on settings driver might or might not attempt to discover endpoints from remote server. +In case when `discovery` option is set to `true` driver will look up server certificate through connection attempt. +The discovery option also enables checks of server endpoints for matching security settings. + +Once initial discovery is completed and driver finds endpoint matching its security settings it will launch second connection attempt. +This attempt will propagate encoding limits described in table above. +Role of these options is declaration of values accepted and expected by client. +Once server returns its limits driver picks minimums of all these. +The only one note is that driver takes minimum of local receive and remote send buffer size. +It does same with local send and remote receive buffer. === Address Format To read, write and subscribe to data, the OPC UA driver uses the variable declaration string of the OPC UA server it is