From 0b1bc6b85c0201ad0879c77de09555ab4213dc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Dywicki?= Date: Mon, 20 Nov 2023 12:20:26 +0100 Subject: [PATCH] feat(plc4j/opcua): Chunking and encryption of request/response calls. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update of protocol logic to reflect chunked payloads. Threading updates within driver itself should allow us to have better control over how results are being processed.. and make error handling more consistent. Signed-off-by: Ɓukasz Dywicki --- .../plc4x/java/opcua/config/Limits.java | 80 ++ .../java/opcua/config/OpcuaConfiguration.java | 79 +- .../context/AsymmetricEncryptionHandler.java | 250 +--- .../opcua/context/BaseEncryptionHandler.java | 239 ++++ .../plc4x/java/opcua/context/CallContext.java | 49 + .../opcua/context/CertificateKeyPair.java | 5 + .../java/opcua/context/Conversation.java | 497 +++++++ .../java/opcua/context/EncryptionHandler.java | 141 +- .../opcua/context/OpcuaDriverContext.java | 30 +- .../java/opcua/context/SecureChannel.java | 1236 ++++------------- .../SecureChannelTransactionManager.java | 88 +- .../context/SymmetricEncryptionHandler.java | 233 +--- .../opcua/protocol/OpcuaProtocolLogic.java | 416 ++---- .../protocol/OpcuaSubscriptionHandle.java | 356 ++--- .../java/opcua/protocol/chunk/Chunk.java | 149 ++ .../opcua/protocol/chunk/ChunkFactory.java | 156 +++ .../opcua/protocol/chunk/ChunkStorage.java | 46 + .../protocol/chunk/MemoryChunkStorage.java | 59 + .../protocol/chunk/PayloadConverter.java | 93 ++ .../java/opcua/security/MessageSecurity.java | 40 + .../java/opcua/security/SecurityPolicy.java | 116 +- .../java/opcua/security/SymmetricKeys.java | 61 +- .../plc4x/java/opcua/OpcuaDriverIT.java | 1 - .../plc4x/java/opcua/OpcuaPlcDriverTest.java | 282 ++-- .../java/opcua/TestCertificateGenerator.java | 69 + .../opcua/context/EncryptionHandlerTest.java | 276 ++++ .../protocol/OpcuaSubscriptionHandleTest.java | 438 +----- .../protocol/chunk/ChunkFactoryTest.java | 154 ++ .../protocol/chunk/PayloadConverterTest.java | 71 + .../milo/examples/server/TestMiloServer.java | 48 +- .../test/resources/chunk-calculation-1024.csv | 49 + .../test/resources/chunk-calculation-2048.csv | 49 + .../test/resources/chunk-calculation-3072.csv | 49 + .../test/resources/chunk-calculation-4096.csv | 49 + .../test/resources/chunk-calculation-5120.csv | 49 + 35 files changed, 3425 insertions(+), 2578 deletions(-) create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/BaseEncryptionHandler.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CallContext.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/Chunk.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactory.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverter.java create mode 100644 plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/MessageSecurity.java create mode 100644 plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/TestCertificateGenerator.java create mode 100644 plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/EncryptionHandlerTest.java create mode 100644 plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactoryTest.java create mode 100644 plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverterTest.java create mode 100644 plc4j/drivers/opcua/src/test/resources/chunk-calculation-1024.csv create mode 100644 plc4j/drivers/opcua/src/test/resources/chunk-calculation-2048.csv create mode 100644 plc4j/drivers/opcua/src/test/resources/chunk-calculation-3072.csv create mode 100644 plc4j/drivers/opcua/src/test/resources/chunk-calculation-4096.csv create mode 100644 plc4j/drivers/opcua/src/test/resources/chunk-calculation-5120.csv 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 new file mode 100644 index 00000000000..3df8ed6d31a --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.config; + +import org.apache.plc4x.java.spi.configuration.Configuration; +import org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter; + +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; + + 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; + + public Limits() { + this(DEFAULT_RECEIVE_BUFFER_SIZE, DEFAULT_SEND_BUFFER_SIZE, DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_MAX_CHUNK_COUNT); + } + + public Limits(int receiveBufferSize, int sendBufferSize, int maxMessageSize, int maxChunkCount) { + this.receiveBufferSize = receiveBufferSize; + this.sendBufferSize = sendBufferSize; + this.maxMessageSize = maxMessageSize; + this.maxChunkCount = maxChunkCount; + } + + public int getReceiveBufferSize() { + return receiveBufferSize; + } + + public int getSendBufferSize() { + return sendBufferSize; + } + + public int getMaxMessageSize() { + return maxMessageSize; + } + + public int getMaxChunkCount() { + return maxChunkCount; + } + + @Override + public String toString() { + return "Limits{" + + " receiveBufferSize=" + receiveBufferSize + + ", sendBufferSize=" + sendBufferSize + + ", maxMessageSize=" + maxMessageSize + + ", maxChunkCount=" + maxChunkCount + + '}'; + } +} 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 f9c8caf0cee..424b7a6f9c3 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 @@ -18,15 +18,25 @@ */ 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; +import org.apache.plc4x.java.spi.configuration.annotations.ComplexConfigurationParameter; import org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter; import org.apache.plc4x.java.spi.configuration.annotations.defaults.BooleanDefaultValue; -import org.apache.plc4x.java.spi.configuration.annotations.defaults.StringDefaultValue; public class OpcuaConfiguration implements Configuration { + public static final long DEFAULT_CONNECTION_LIFETIME = 36000000; + + public static final long DEFAULT_REQUEST_TIMEOUT = 5 * 60 * 1000L; + + public static final long DEFAULT_SESSION_TIMEOUT = 120000L; + @ConfigurationParameter("protocolCode") private String protocolCode; @@ -49,6 +59,9 @@ public class OpcuaConfiguration implements Configuration { @ConfigurationParameter("securityPolicy") private SecurityPolicy securityPolicy = SecurityPolicy.NONE; + @ConfigurationParameter("messageSecurity") + private MessageSecurity messageSecurity = MessageSecurity.SIGN_ENCRYPT; + @ConfigurationParameter("keyStoreFile") private String keyStoreFile; @@ -57,8 +70,22 @@ public class OpcuaConfiguration implements Configuration { @ConfigurationParameter("keyStorePassword") private String keyStorePassword; - private byte[] senderCertificate; - private PascalByteString thumbprint; + private X509Certificate serverCertificate; + + @ConfigurationParameter("channelLifetime") + private long channelLifetime = DEFAULT_CONNECTION_LIFETIME; + + @ConfigurationParameter("requestTimeout") + private long requestTimeout = DEFAULT_REQUEST_TIMEOUT; + + @ConfigurationParameter("sessionTimeout") + private long sessionTimeout = DEFAULT_SESSION_TIMEOUT; + + @ConfigurationParameter("openChannelTimeout") + private long openChannelTimeout = DEFAULT_REQUEST_TIMEOUT; + + @ComplexConfigurationParameter(prefix = "encoding", defaultOverrides = {}, requiredOverrides = {}) + private Limits limits = new Limits(); public String getProtocolCode() { return protocolCode; @@ -92,6 +119,10 @@ public SecurityPolicy getSecurityPolicy() { return securityPolicy; } + public MessageSecurity getMessageSecurity() { + return messageSecurity; + } + public String getKeyStoreFile() { return keyStoreFile; } @@ -100,6 +131,30 @@ public String getKeyStorePassword() { return keyStorePassword; } + public Limits getEncodingLimits() { + return limits; + } + + public X509Certificate getServerCertificate() { + return serverCertificate; + } + + public void setServerCertificate(X509Certificate serverCertificate) { + this.serverCertificate = serverCertificate; + } + + public long getChannelLifetime() { + return channelLifetime; + } + + public long getRequestTimeout() { + return requestTimeout; + } + + public long getOpenChannelTimeout() { + return openChannelTimeout; + } + @Override public String toString() { return "OpcuaConfiguration{" + @@ -110,24 +165,8 @@ public String toString() { ", keyStoreFile='" + keyStoreFile + '\'' + ", certDirectory='" + certDirectory + '\'' + ", keyStorePassword='" + (keyStorePassword != null ? "******" : null) + '\'' + + ", limits=" + limits + '}'; } - - public byte[] getSenderCertificate() { - return senderCertificate; - } - - public void setSenderCertificate(byte[] senderCertificate) { - this.senderCertificate = senderCertificate; - } - - public PascalByteString getThumbprint() { - return this.thumbprint; - } - - public void setThumbprint(PascalByteString thumbprint) { - this.thumbprint = thumbprint; - } - } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/AsymmetricEncryptionHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/AsymmetricEncryptionHandler.java index e537bc1b551..7d8ba1009a4 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/AsymmetricEncryptionHandler.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/AsymmetricEncryptionHandler.java @@ -1,208 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ package org.apache.plc4x.java.opcua.context; -import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; -import org.apache.plc4x.java.opcua.readwrite.MessagePDU; -import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU; -import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenResponse; -import org.apache.plc4x.java.opcua.security.SecurityPolicy; -import org.apache.plc4x.java.spi.generation.*; - -import javax.crypto.Cipher; -import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.Signature; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPublicKey; - -public class AsymmetricEncryptionHandler { - private static final int SECURE_MESSAGE_HEADER_SIZE = 12; - private static final int SEQUENCE_HEADER_SIZE = 8; - - private final SecurityPolicy policy; +import java.security.SignatureException; +import javax.crypto.Cipher; +import org.apache.plc4x.java.opcua.protocol.chunk.Chunk; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.apache.plc4x.java.spi.generation.WriteBufferByteBased; - private final X509Certificate serverCertificate; - private final X509Certificate clientCertificate; +public class AsymmetricEncryptionHandler extends BaseEncryptionHandler { - private final PrivateKey clientPrivateKey; + private final PrivateKey senderPrivateKey; - public AsymmetricEncryptionHandler(X509Certificate serverCertificate, X509Certificate clientCertificate, PrivateKey clientPrivateKey, PublicKey clientPublicKey, SecurityPolicy policy) { - this.serverCertificate = serverCertificate; - this.clientCertificate = clientCertificate; - this.clientPrivateKey = clientPrivateKey; - this.policy = policy; + public AsymmetricEncryptionHandler(Conversation conversation, SecurityPolicy securityPolicy, PrivateKey senderPrivateKey) { + super(conversation, securityPolicy); + this.senderPrivateKey = senderPrivateKey; } - /** - * Docs: https://reference.opcfoundation.org/Core/Part6/v104/docs/6.7 - * - * @param pdu - * @param message - * @return - */ - public ReadBuffer encodeMessage(MessagePDU pdu, byte[] message) { - int unencryptedLength = pdu.getLengthInBytes(); - int messageLength = message.length; - - int beforeBodyLength = unencryptedLength - messageLength; // message header, security header, sequence header - - int cipherTextBlockSize = (getAsymmetricKeyLength(serverCertificate) + 7) / 8; - int plainTextBlockSize = (getAsymmetricKeyLength(serverCertificate) + 7) / 8 - policy.getAsymmetricPlainBlock(); - int signatureSize = (getAsymmetricKeyLength(clientCertificate) + 7) / 8; - - - int maxChunkSize = 8196; - int paddingOverhead = cipherTextBlockSize > 256 ? 2 : 1; - - - int securityHeaderSize = beforeBodyLength - SEQUENCE_HEADER_SIZE - SECURE_MESSAGE_HEADER_SIZE; - int maxCipherTextSize = maxChunkSize - securityHeaderSize; - int maxCipherTextBlocks = maxCipherTextSize / cipherTextBlockSize; - int maxPlainTextSize = maxCipherTextBlocks * plainTextBlockSize; - int maxBodySize = maxPlainTextSize - SEQUENCE_HEADER_SIZE - paddingOverhead - signatureSize; - - int bodySize = Math.min(message.length, maxBodySize); - - int plainTextSize = SEQUENCE_HEADER_SIZE + bodySize + paddingOverhead + signatureSize; - int remaining = plainTextSize % plainTextBlockSize; - int paddingSize = remaining > 0 ? plainTextBlockSize - remaining : 0; - - int plainTextContentSize = SEQUENCE_HEADER_SIZE + bodySize + - signatureSize + paddingSize + paddingOverhead; - - int frameSize = SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize + - (plainTextContentSize / plainTextBlockSize) * cipherTextBlockSize; - - try { - WriteBufferByteBased buf = new WriteBufferByteBased(frameSize, ByteOrder.LITTLE_ENDIAN); - OpcuaAPU opcuaAPU = new OpcuaAPU(pdu); - opcuaAPU.serialize(buf); - - writePadding(paddingSize, buf); - updateFrameSize(frameSize, buf); - - byte[] sign = sign(buf.getBytes()); - buf.writeByteArray(sign); - - buf.setPos(SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize); - - int blockCount = (frameSize - buf.getPos()) / plainTextBlockSize;// -> plainTextContentSize / plainTextBlockSize - - byte[] encrypted = encrypt(plainTextBlockSize, securityHeaderSize, frameSize, buf, blockCount); - buf.writeByteArray(encrypted); + protected void verify(WriteBufferByteBased buffer, Chunk chunk, int messageLength) throws Exception { + int signatureStart = messageLength - chunk.getSignatureSize(); + byte[] message = buffer.getBytes(0, signatureStart); + byte[] signatureData = buffer.getBytes(signatureStart, signatureStart + chunk.getSignatureSize()); - return new ReadBufferByteBased(buf.getBytes(), ByteOrder.LITTLE_ENDIAN); - } catch (Exception e) { - throw new RuntimeException(e); + Signature signature = securityPolicy.getAsymmetricSignatureAlgorithm().getSignature(); + signature.initVerify(conversation.getRemoteCertificate().getPublicKey()); + signature.update(message); + if (signature.verify(signatureData)) { + throw new IllegalArgumentException("Invalid signature"); } } - public OpcuaAPU decodeMessage(OpcuaAPU pdu) { - MessagePDU message = pdu.getMessage(); + protected int decrypt(WriteBufferByteBased chunkBuffer, Chunk chunk, int messageLength) throws Exception { + int bodyStart = 12 + chunk.getSecurityHeaderSize(); - OpcuaOpenResponse a = (OpcuaOpenResponse) message; + int bodySize = messageLength - bodyStart; + int blockCount = bodySize / chunk.getCipherTextBlockSize(); + assert(bodySize % chunk.getCipherTextBlockSize() == 0); + byte[] encrypted = chunkBuffer.getBytes(bodyStart, bodyStart + bodySize); + byte[] plainText = new byte[chunk.getCipherTextBlockSize() * blockCount]; - int cipherTextBlockSize = (getAsymmetricKeyLength(serverCertificate) + 7) / 8; - int signatureSize = (getAsymmetricKeyLength(clientCertificate) + 7) / 8; + Cipher cipher = securityPolicy.getAsymmetricEncryptionAlgorithm().getCipher(); + cipher.init(Cipher.DECRYPT_MODE, senderPrivateKey); - if (!(a.getMessage() instanceof BinaryPayload)) { - throw new IllegalArgumentException("Unexpected payload"); - } - byte[] textMessage = ((BinaryPayload) a.getMessage()).getPayload(); - - int blockCount = (SEQUENCE_HEADER_SIZE + textMessage.length) / cipherTextBlockSize; - int plainTextBufferSize = cipherTextBlockSize * blockCount; - - try { - WriteBufferByteBased buf = new WriteBufferByteBased(pdu.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - pdu.serialize(buf); - - Cipher cipher = policy.getAsymmetricEncryptionAlgorithm().getCipher(); - cipher.init(Cipher.DECRYPT_MODE, clientPrivateKey); - - ByteBuffer buffer = ByteBuffer.allocate(plainTextBufferSize); - byte[] bytes = buf.getBytes(pdu.getLengthInBytes() - plainTextBufferSize, pdu.getLengthInBytes()); - //byte[] bytes = textMessage; - ByteBuffer originalMessage = ByteBuffer.wrap(bytes); - - for (int blockNumber = 0; blockNumber < blockCount; blockNumber++) { - originalMessage.limit(originalMessage.position() + cipherTextBlockSize); - cipher.doFinal(originalMessage, buffer); - } - - buffer.flip(); - buf.setPos(pdu.getLengthInBytes() - plainTextBufferSize); - buf.writeByteArray(buffer.array()); - int frameSize = pdu.getLengthInBytes() - plainTextBufferSize + buffer.limit(); - updateFrameSize(frameSize, buf); - - byte[] decryptedMessage = buf.getBytes(0, frameSize); + int bodyLength = 0; + for (int block = 0; block < blockCount; block++) { + int pos = block * chunk.getCipherTextBlockSize(); - ReadBuffer readBuffer = new ReadBufferByteBased(decryptedMessage, ByteOrder.LITTLE_ENDIAN); - OpcuaAPU opcuaAPU = OpcuaAPU.staticParse(readBuffer, true); - return opcuaAPU; - } catch (Exception e) { - throw new RuntimeException(e); + bodyLength += cipher.doFinal(encrypted, pos, chunk.getCipherTextBlockSize(), plainText, pos); } - + chunkBuffer.setPos(bodyStart); + byte[] decrypted = new byte[bodyLength]; + System.arraycopy(plainText, 0, decrypted, 0, bodyLength); + chunkBuffer.writeByteArray("payload", decrypted); + return bodyLength; } - private byte[] encrypt(int plainTextBlockSize, int securityHeaderSize, int frameSize, WriteBufferByteBased buf, int blockCount) throws Exception { - ByteBuffer buffer = ByteBuffer.allocate(frameSize - buf.getPos()); - ByteBuffer originalMessage = ByteBuffer.wrap(buf.getBytes(SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize, frameSize)); - + protected void encrypt(WriteBufferByteBased buffer, int securityHeaderSize, int plainTextBlockSize, int cipherTextBlockSize, int blockCount) throws Exception { + int bodyStart = 12 + securityHeaderSize; + byte[] copy = buffer.getBytes(bodyStart, bodyStart + (plainTextBlockSize * blockCount)); + byte[] encrypted = new byte[cipherTextBlockSize * blockCount]; - Cipher cipher = policy.getAsymmetricEncryptionAlgorithm().getCipher(); - cipher.init(Cipher.ENCRYPT_MODE, serverCertificate.getPublicKey()); + // copy of bytes from sequence header over payload, padding bytes and signature + Cipher cipher = securityPolicy.getAsymmetricEncryptionAlgorithm().getCipher(); + cipher.init(Cipher.ENCRYPT_MODE, conversation.getRemoteCertificate().getPublicKey()); for (int block = 0; block < blockCount; block++) { - int position = block * plainTextBlockSize; - int limit = (block + 1) * plainTextBlockSize; - originalMessage.position(position); - originalMessage.limit(limit); - - cipher.doFinal(originalMessage, buffer); + int pos = block * plainTextBlockSize; + int target = block * cipherTextBlockSize; + cipher.doFinal(copy, pos, plainTextBlockSize, encrypted, target); } - return buffer.array(); - } - - private static void updateFrameSize(int frameSize, WriteBufferByteBased buf) throws SerializationException { - int initPosition = buf.getPos(); - buf.setPos(4); - buf.writeInt(32, frameSize); - buf.setPos(initPosition); - } - - public byte[] sign(byte[] data) { - try { - Signature signature = policy.getAsymmetricSignatureAlgorithm().getSignature(); - signature.initSign(clientPrivateKey); - signature.update(data); - return signature.sign(); - } catch (Exception e) { - throw new RuntimeException(e); - } + buffer.setPos(bodyStart); + buffer.writeByteArray("encrypted", encrypted); } - private void writePadding(int paddingSize, WriteBufferByteBased buffer) throws Exception { - buffer.writeByte((byte) paddingSize); - for (int i = 0; i < paddingSize; i++) { - buffer.writeByte((byte) paddingSize); - } - } - - - static int getAsymmetricKeyLength(Certificate certificate) { - PublicKey publicKey = certificate != null ? - certificate.getPublicKey() : null; - - return (publicKey instanceof RSAPublicKey) ? - ((RSAPublicKey) publicKey).getModulus().bitLength() : 0; + public byte[] sign(byte[] contentsToSign) throws GeneralSecurityException { + Signature signature = securityPolicy.getAsymmetricSignatureAlgorithm().getSignature(); + signature.initSign(senderPrivateKey); + signature.update(contentsToSign); + return signature.sign(); } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/BaseEncryptionHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/BaseEncryptionHandler.java new file mode 100644 index 00000000000..91b7a686ff3 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/BaseEncryptionHandler.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.context; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import org.apache.plc4x.java.opcua.protocol.chunk.Chunk; +import org.apache.plc4x.java.opcua.protocol.chunk.PayloadConverter; +import org.apache.plc4x.java.opcua.readwrite.ChunkType; +import org.apache.plc4x.java.opcua.readwrite.MessagePDU; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.apache.plc4x.java.spi.generation.ByteOrder; +import org.apache.plc4x.java.spi.generation.SerializationException; +import org.apache.plc4x.java.spi.generation.WriteBufferByteBased; + +abstract class BaseEncryptionHandler { + + protected static final int SECURE_MESSAGE_HEADER_SIZE = 12; + protected static final int SEQUENCE_HEADER_SIZE = 8; + + protected final Conversation conversation; + protected final SecurityPolicy securityPolicy; + + public BaseEncryptionHandler(Conversation conversation, SecurityPolicy securityPolicy) { + this.conversation = conversation; + this.securityPolicy = securityPolicy; + } + public final List encodeMessage(Chunk chunk, MessagePDU message, Supplier sequenceSupplier) { + + try { + ByteBuffer messageBuffer = ByteBuffer.wrap(PayloadConverter.toStream(message)); + int sequenceStart = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize(); + + // processed parts of frame + byte[] messageHeader = new byte[SECURE_MESSAGE_HEADER_SIZE]; + messageBuffer.get(messageHeader); + byte[] securityHeader = new byte[chunk.getSecurityHeaderSize()]; + messageBuffer.get(securityHeader); + byte[] sequenceHeader = new byte[SEQUENCE_HEADER_SIZE]; + messageBuffer.get(sequenceHeader); + + ByteBuffer bodyBuffer = messageBuffer.slice(); + List messages = new ArrayList<>(); + boolean first = true; + while (bodyBuffer.hasRemaining()) { + int bodySize = Math.min(bodyBuffer.remaining(), chunk.getMaxBodySize()); + int paddingSize = 0; + if (chunk.isEncrypted()) { + int plainTextSize = SEQUENCE_HEADER_SIZE + bodySize + chunk.getPaddingOverhead() + chunk.getSignatureSize(); + int gap = plainTextSize % chunk.getPlainTextBlockSize(); + paddingSize = gap > 0 ? chunk.getPlainTextBlockSize() - gap : 0; + } + + int plainTextContentSize = SEQUENCE_HEADER_SIZE + bodySize + chunk.getSignatureSize() + paddingSize + chunk.getPaddingOverhead(); + if (chunk.isEncrypted()) { + assert ((plainTextContentSize % chunk.getPlainTextBlockSize()) == 0); + } + + int chunkSize = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize() + (plainTextContentSize / chunk.getPlainTextBlockSize()) * chunk.getCipherTextBlockSize(); + + WriteBufferByteBased chunkBuffer = new WriteBufferByteBased(chunkSize, ByteOrder.LITTLE_ENDIAN); + chunkBuffer.writeByteArray("messageHeader", messageHeader); + chunkBuffer.writeByteArray("securityHeader", securityHeader); + chunkBuffer.writeByteArray("sequenceHeader", sequenceHeader); + updateFrameSize(chunkBuffer, chunkSize); + ChunkType chunkType = bodyBuffer.remaining() - bodySize > 0 ? ChunkType.CONTINUE : ChunkType.FINAL; + updateFrame(first, chunkBuffer, chunk, chunkType, sequenceSupplier); // populate headers + first = false; + + byte[] chunkContents = new byte[bodySize]; + bodyBuffer.get(chunkContents); + // copy part of message not larger than body size into chunk buffer + chunkBuffer.writeByteArray("payload", chunkContents); + + if (chunk.isEncrypted()) { + for (int index = 0, limit = paddingSize + chunk.getPaddingOverhead(); index < limit; index++) { + chunkBuffer.writeByte("padding", (byte) paddingSize); + } + if (chunk.getPaddingOverhead() > 1) { + // override extra padding byte with MSB of padding size + chunkBuffer.setPos(bodySize + paddingSize + chunk.getPaddingOverhead()); + chunkBuffer.writeByte("paddingMSB", (byte) ((paddingSize >> 8) & 0xFF)); + } + } + + if (chunk.isSigned()) { + byte[] signatureData = sign(chunkBuffer.getBytes(0, chunkBuffer.getPos())); + chunkBuffer.writeByteArray("signature", signatureData); + } + if (chunk.isEncrypted()) { + encrypt(chunkBuffer, chunk.getSecurityHeaderSize(), chunk.getPlainTextBlockSize(), + chunk.getCipherTextBlockSize(), plainTextContentSize / chunk.getPlainTextBlockSize() + ); + } + + MessagePDU chunkedMessage = PayloadConverter.pduFromStream(chunkBuffer.getBytes(), message.getResponse()); + messages.add(chunkedMessage); + } + return messages; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public final MessagePDU decodeMessage(Chunk chunk, MessagePDU message) { + try { + if (!chunk.isEncrypted() && !chunk.isSigned()) { + return message; + } + + int messageLength = message.getLengthInBytes(); + WriteBufferByteBased chunkBuffer = new WriteBufferByteBased(messageLength, ByteOrder.LITTLE_ENDIAN); + message.serialize(chunkBuffer); + + int bodySize = messageLength - chunk.getSecurityHeaderSize() - SECURE_MESSAGE_HEADER_SIZE; + if (chunk.isEncrypted()) { + bodySize = decrypt(chunkBuffer, chunk, messageLength); + } + + if (chunk.isSigned()) { + verify(chunkBuffer, chunk, messageLength); + } + + int encryptionOverhead = getEncryptionOverhead(chunk, messageLength); + int paddingSize = getPaddingSize(chunkBuffer, chunk, messageLength); + + int payloadStart = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize(); + int payloadEnd = payloadStart + bodySize - paddingSize - chunk.getSignatureSize() - chunk.getPaddingOverhead(); + int expectedPaddingSize = messageLength - payloadEnd - chunk.getSignatureSize() - encryptionOverhead - chunk.getPaddingOverhead(); + + if (paddingSize != expectedPaddingSize) { + throw new IllegalArgumentException("Malformed data detected - expected padding size do not match"); + } + + if (chunk.isEncrypted()) { + byte[] paddingBytes = chunkBuffer.getBytes(payloadEnd, payloadEnd + expectedPaddingSize); + byte paddingByte = (byte) (paddingSize & 0xff); + for (int index = 0; index < paddingBytes.length; index++) { + if (paddingBytes[index] != paddingByte) { + throw new IllegalArgumentException("Malformed padding byte at index " + index); + } + } + } + + int overhead = paddingSize + chunk.getSignatureSize() + chunk.getPaddingOverhead() + encryptionOverhead; + updateFrameSize(chunkBuffer, messageLength - overhead); + + return PayloadConverter.pduFromStream(chunkBuffer.getBytes(), message.getResponse()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void updateFrame(boolean first, WriteBufferByteBased messageBuffer, Chunk chunk, ChunkType chunkType, Supplier sequenceSupplier) throws SerializationException { + int payloadStart = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize(); + if (chunkType != ChunkType.FINAL) { + messageBuffer.setPos(3); + messageBuffer.writeString("chunkType", 8, chunkType.getValue()); + } + + if (!first) { + messageBuffer.setPos(payloadStart); + messageBuffer.writeUnsignedLong("sequenceId", 32, sequenceSupplier.get()); + } + + // leave buffer at beginning of message body + messageBuffer.setPos(payloadStart + 8); + } + + private void updateFrameSize(WriteBufferByteBased messageBuffer, long frameSize) throws SerializationException { + int position = messageBuffer.getPos(); + try { + messageBuffer.setPos(4); + messageBuffer.writeUnsignedLong("totalLength", 32, frameSize); + } finally { + messageBuffer.setPos(position); + } + } + + private int getEncryptionOverhead(Chunk chunk, int messageLength) { + if (!chunk.isEncrypted()) { + return 0; + } + + int bodyStart = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize(); + int bodySize = messageLength - bodyStart; + int blockCount = bodySize / chunk.getCipherTextBlockSize(); + // bytes we "lost" after payload got decrypted + return (chunk.getCipherTextBlockSize() * blockCount) - (chunk.getPlainTextBlockSize() * blockCount); + } + + private short getPaddingSize(WriteBufferByteBased chunkBuffer, Chunk chunk, int messageLength) { + if (!chunk.isEncrypted()) { + return 0; + } + + int bodyStart = SECURE_MESSAGE_HEADER_SIZE + chunk.getSecurityHeaderSize(); + int bodySize = messageLength - bodyStart; + int blockCount = bodySize / chunk.getCipherTextBlockSize(); + // bytes we "lost" after payload got decrypted + int encryptionOverhead = (chunk.getCipherTextBlockSize() * blockCount) - (chunk.getPlainTextBlockSize() * blockCount); + + int paddingEnd = messageLength - chunk.getSignatureSize() - encryptionOverhead - chunk.getPaddingOverhead(); + byte[] padding = chunkBuffer.getBytes(paddingEnd, paddingEnd + chunk.getPaddingOverhead()); + if (padding.length > 2) { // cipher block size exceeds 256 bytes + return (short)(((padding[1] & 0xFF) << 8) | (padding[0] & 0xFF)); + } + return padding[0]; + } + + protected abstract void verify(WriteBufferByteBased buffer, Chunk chunk, int messageLength) throws Exception; + + protected abstract int decrypt(WriteBufferByteBased chunkBuffer, Chunk chunk, int messageLength) throws Exception; + + protected abstract void encrypt(WriteBufferByteBased buffer, int securityHeaderSize, int plainTextBlockSize, int cipherTextBlockSize, int blockCount) throws Exception; + + protected abstract byte[] sign(byte[] contentsToSign) throws GeneralSecurityException; + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CallContext.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CallContext.java new file mode 100644 index 00000000000..266d9531fa6 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CallContext.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.context; + +import java.util.function.Supplier; +import org.apache.plc4x.java.opcua.protocol.chunk.ChunkStorage; +import org.apache.plc4x.java.opcua.readwrite.SecurityHeader; + +public class CallContext { + + private final SecurityHeader sequenceHeader; + private final Supplier sequenceSupplier; + private final int requestId; + + public CallContext(SecurityHeader sequenceHeader, Supplier sequenceSupplier, int requestId) { + this.sequenceHeader = sequenceHeader; + this.sequenceSupplier = sequenceSupplier; + this.requestId = requestId; + } + + public SecurityHeader getSecurityHeader() { + return sequenceHeader; + } + + public int getNextSequenceNumber() { + return sequenceSupplier.get(); + } + + public int getRequestId() { + return requestId; + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java index 9133a68bf44..f166d77ba44 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java @@ -19,6 +19,7 @@ package org.apache.plc4x.java.opcua.context; import io.vavr.control.Try; +import java.security.PrivateKey; import org.bouncycastle.asn1.x509.GeneralName; import java.security.KeyPair; @@ -49,6 +50,10 @@ public X509Certificate getCertificate() { return certificate; } + public PrivateKey getPrivateKey() { + return keyPair.getPrivate(); + } + public byte[] getThumbPrint() { return thumbprint; } 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 new file mode 100644 index 00000000000..94df2cdbcc8 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java @@ -0,0 +1,497 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.context; + +import static org.apache.plc4x.java.opcua.readwrite.ChunkType.ABORT; +import static org.apache.plc4x.java.opcua.readwrite.ChunkType.FINAL; + +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; +import org.apache.commons.lang3.RandomUtils; +import org.apache.plc4x.java.api.exceptions.PlcProtocolException; +import org.apache.plc4x.java.opcua.config.Limits; +import org.apache.plc4x.java.opcua.config.OpcuaConfiguration; +import org.apache.plc4x.java.opcua.protocol.chunk.ChunkStorage; +import org.apache.plc4x.java.opcua.protocol.chunk.MemoryChunkStorage; +import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; +import org.apache.plc4x.java.opcua.readwrite.ChunkType; +import org.apache.plc4x.java.opcua.readwrite.ExpandedNodeId; +import org.apache.plc4x.java.opcua.readwrite.ExtensiblePayload; +import org.apache.plc4x.java.opcua.readwrite.ExtensionObject; +import org.apache.plc4x.java.opcua.readwrite.ExtensionObjectDefinition; +import org.apache.plc4x.java.opcua.readwrite.ExtensionObjectEncodingMask; +import org.apache.plc4x.java.opcua.readwrite.MessagePDU; +import org.apache.plc4x.java.opcua.readwrite.NodeId; +import org.apache.plc4x.java.opcua.readwrite.NodeIdFourByte; +import org.apache.plc4x.java.opcua.readwrite.NodeIdTwoByte; +import org.apache.plc4x.java.opcua.readwrite.NodeIdTypeDefinition; +import org.apache.plc4x.java.opcua.readwrite.NullExtension; +import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU; +import org.apache.plc4x.java.opcua.readwrite.OpcuaAcknowledgeResponse; +import org.apache.plc4x.java.opcua.readwrite.OpcuaCloseRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaConstants; +import org.apache.plc4x.java.opcua.readwrite.OpcuaHelloRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageResponse; +import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenResponse; +import org.apache.plc4x.java.opcua.readwrite.OpcuaProtocolLimits; +import org.apache.plc4x.java.opcua.readwrite.OpcuaStatusCode; +import org.apache.plc4x.java.opcua.readwrite.PascalString; +import org.apache.plc4x.java.opcua.readwrite.Payload; +import org.apache.plc4x.java.opcua.readwrite.RequestHeader; +import org.apache.plc4x.java.opcua.readwrite.ResponseHeader; +import org.apache.plc4x.java.opcua.readwrite.SecurityHeader; +import org.apache.plc4x.java.opcua.readwrite.SequenceHeader; +import org.apache.plc4x.java.opcua.readwrite.ServiceFault; +import org.apache.plc4x.java.opcua.readwrite.SignatureData; +import org.apache.plc4x.java.opcua.security.MessageSecurity; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.apache.plc4x.java.spi.ConversationContext; +import org.apache.plc4x.java.spi.ConversationContext.SendRequestContext; +import org.apache.plc4x.java.spi.generation.ParseException; +import org.apache.plc4x.java.spi.generation.ReadBufferByteBased; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Conversation { + private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time. + + private static final ExpandedNodeId NULL_EXPANDED_NODE_ID = new ExpandedNodeId(false, + false, + new NodeIdTwoByte((short) 0), + null, + null + ); + + protected static final ExtensionObject NULL_EXTENSION_OBJECT = new ExtensionObject( + NULL_EXPANDED_NODE_ID, + new ExtensionObjectEncodingMask(false, false, false), + new NullExtension()); // Body + + + private final Logger logger = LoggerFactory.getLogger(Conversation.class); + private final AtomicReference securityHeader = new AtomicReference<>(new SecurityHeader(1, 1)); + private final AtomicLong senderSequenceNumber = new AtomicLong(-1); + + private final AtomicReference authenticationToken = new AtomicReference<>(new NodeIdTwoByte((short) 0)); + + private final ConversationContext context; + private final SecureChannelTransactionManager tm; + + private final SecurityPolicy securityPolicy; + private final MessageSecurity messageSecurity; + private final EncryptionHandler encryptionHandler; + private final OpcuaDriverContext driverContext; + private final OpcuaConfiguration configuration; + + private OpcuaProtocolLimits limits; + + private X509Certificate localCertificate = null; + private X509Certificate remoteCertificate = null; + private byte[] remoteNonce; + private byte[] localNonce; + + private final BiPredicate> sequenceValidator = (sequenceHeader, callback) -> { + if (senderSequenceNumber.get() == -1L) { + senderSequenceNumber.set(sequenceHeader.getSequenceNumber()); + return true; + } + int expectedSequence = sequenceHeader.getSequenceNumber() - 1; + if (!senderSequenceNumber.compareAndSet(expectedSequence, sequenceHeader.getSequenceNumber())) { + callback.completeExceptionally( + new PlcProtocolException("Lost sequence, expected " + expectedSequence + " but received " + sequenceHeader.getSequenceNumber()) + ); + return false; + } + return true; + }; + + public Conversation(ConversationContext context, OpcuaDriverContext driverContext, OpcuaConfiguration configuration) { + this.context = context; + this.tm = new SecureChannelTransactionManager(); + this.driverContext = driverContext; + this.configuration = configuration; + + this.securityPolicy = determineSecurityPolicy(configuration); + CertificateKeyPair senderKeyPair = driverContext.getCertificateKeyPair(); + + if (this.securityPolicy != SecurityPolicy.NONE) { + //Sender Certificate gets populated during the 'discover' phase when encryption is enabled. + this.messageSecurity = configuration.getMessageSecurity(); + this.remoteCertificate = configuration.getServerCertificate(); + this.encryptionHandler = new EncryptionHandler(this, senderKeyPair.getPrivateKey()); + this.localCertificate = senderKeyPair.getCertificate(); + this.localNonce = createNonce(); + } else { + this.messageSecurity = MessageSecurity.NONE; + this.encryptionHandler = new EncryptionHandler(this, null); + } + + Limits encodingLimits = configuration.getEncodingLimits(); + limits = new OpcuaProtocolLimits( + encodingLimits.getReceiveBufferSize(), + encodingLimits.getSendBufferSize(), + encodingLimits.getMaxMessageSize(), + encodingLimits.getMaxChunkCount() + ); + } + + public CompletableFuture requestHello() { + logger.debug("Sending hello message to {}", this.driverContext.getEndpoint()); + OpcuaHelloRequest request = new OpcuaHelloRequest(FINAL, + OpcuaConstants.PROTOCOLVERSION, + new OpcuaProtocolLimits( + limits.getReceiveBufferSize(), + limits.getSendBufferSize(), + limits.getMaxMessageSize(), + limits.getMaxChunkCount() + ), + new PascalString(driverContext.getEndpoint()) + ); + + // open messages are guaranteed to fit into 8192 bytes limit + //CompletableFuture future = new CompletableFuture<>(); + + CompletableFuture future = new CompletableFuture<>(); + sendRequest(request, future, configuration.getOpenChannelTimeout()) + .unwrap(OpcuaAPU::getMessage) + .check(OpcuaAcknowledgeResponse.class::isInstance) + .unwrap(OpcuaAcknowledgeResponse.class::cast) + .handle(opcuaAcknowledgeResponse -> { + OpcuaProtocolLimits limits = opcuaAcknowledgeResponse.getLimits(); + // merge encoding limits to match common minimum: + // our receipt buffer should not exceed server send buffer size, + // our send buffer size should not exceed server receive buffer size + // chunks and message sizes should match too + this.limits = new OpcuaProtocolLimits( + Math.min(this.limits.getReceiveBufferSize(), limits.getSendBufferSize()), + Math.min(this.limits.getSendBufferSize(), limits.getReceiveBufferSize()), + Math.min(this.limits.getMaxMessageSize(), limits.getMaxMessageSize()), + Math.min(this.limits.getMaxChunkCount(), limits.getMaxChunkCount()) + ); + future.complete(opcuaAcknowledgeResponse); + }); + return future; + } + + public CompletableFuture requestChannelOpen(Function request) { + return request( + OpcuaOpenResponse.class, request, + (rsp, chunk) -> new OpcuaOpenResponse(rsp.getChunk(), rsp.getOpenResponse(), chunk), + (rsp) -> rsp.getMessage().getSequenceHeader(), + OpcuaOpenResponse::getMessage + ); + } + + public CompletableFuture requestChannelClose(Function request) { + logger.trace("Got close secure channel request"); + return request( + OpcuaMessageResponse.class, request, + (rsp, chunk) -> new OpcuaMessageResponse(rsp.getChunk(), rsp.getSecurityHeader(), chunk), + (rsp) -> rsp.getMessage().getSequenceHeader(), + OpcuaMessageResponse::getMessage + ).whenComplete((r, e) -> { + context.fireDisconnected(); + }).thenApply(r -> null); + } + + private CompletableFuture request( + Class replyType, Function request, + BiFunction chunkAssembler, + Function sequenceHeaderExtractor, + Function chunkExtractor + ) { + int requestId = tm.getTransactionIdentifier(); + logger.debug("Firing request {}", requestId); + T messagePDU = request.apply( + new CallContext(securityHeader.get(), tm.getSequenceSupplier(), requestId) + ); + + MemoryChunkStorage chunkStorage = new MemoryChunkStorage(); + List chunks = encryptionHandler.encodeMessage(messagePDU, tm.getSequenceSupplier()); + CompletableFuture future = new CompletableFuture<>(); + for (int count = chunks.size(), index = 0; index < count; index++) { + boolean last = index + 1 == count; + if (last) { + sendRequest(chunks.get(index), future, configuration.getRequestTimeout()) + .unwrap(OpcuaAPU::getMessage) + .check(replyType::isInstance) + .unwrap(replyType::cast) + .unwrap(msg -> encryptionHandler.decodeMessage(msg)) + .check(replyType::isInstance) + .unwrap(replyType::cast) + .check(reply -> requestId == sequenceHeaderExtractor.apply(reply).getRequestId()) + .check(reply -> sequenceValidator.test(sequenceHeaderExtractor.apply(reply), future)) + .check(msg -> accumulateChunkUntilFinal(chunkStorage, msg.getChunk(), chunkExtractor.apply(msg))) + .unwrap(msg -> mergeChunks(chunkStorage, msg, sequenceHeaderExtractor.apply(msg), chunkAssembler)) + .handle(response -> { + future.complete(response); + }); + } else { + context.sendToWire(new OpcuaAPU(chunks.get(index))); + } + } + return future; + } + + public CompletableFuture submit(T object, Class replyType) { + return submit(object).thenApply(response -> { + if (replyType.isInstance(response)) { + return replyType.cast(response); + } + throw new IllegalStateException("Received reply of unexpected type " + response.getClass().getName() + " while " + replyType.getName() + " has been expected"); + }); + } + + private CompletableFuture submit(ExtensionObjectDefinition requestDefinition) { + Integer requestId = tm.getTransactionIdentifier(); + + ExpandedNodeId expandedNodeId = new ExpandedNodeId( + false, //Namespace Uri Specified + false, //Server Index Specified + new NodeIdFourByte((short) 0, Integer.parseInt(requestDefinition.getIdentifier())), + null, + null + ); + ExtensionObject requestObject = new ExtensionObject(expandedNodeId, null, requestDefinition); + ExtensiblePayload payload = new ExtensiblePayload( + new SequenceHeader(tm.getSequenceSupplier().get(), requestId), + requestObject + ); + + MemoryChunkStorage chunkStorage = new MemoryChunkStorage(); + SecurityHeader securityHeaderValue = securityHeader.get(); + OpcuaMessageRequest request = new OpcuaMessageRequest(FINAL, securityHeaderValue, payload); + + logger.debug("Submitting Transaction to TransactionManager {}, security channel {}, token {}", requestId, + securityHeaderValue.getSecureChannelId(), securityHeaderValue.getSecureTokenId()); + + List chunks = encryptionHandler.encodeMessage(request, tm.getSequenceSupplier()); + CompletableFuture future = new CompletableFuture<>(); + for (int count = chunks.size(), index = 0; index < count; index++) { + boolean last = index + 1 == count; + if (last) { + BiFunction chunkAssembler = (src, chunkPayload) -> + new OpcuaMessageResponse(src.getChunk(), src.getSecurityHeader(), chunkPayload); + + sendRequest(chunks.get(index), future, configuration.getRequestTimeout()) + .unwrap(OpcuaAPU::getMessage) + .check(OpcuaMessageResponse.class::isInstance) + .unwrap(OpcuaMessageResponse.class::cast) + .unwrap(msg -> encryptionHandler.decodeMessage(msg)) + .check(OpcuaMessageResponse.class::isInstance) + .unwrap(OpcuaMessageResponse.class::cast) + .check(OpcuaMessageResponse.class::isInstance) + .unwrap(OpcuaMessageResponse.class::cast) + .check(msg -> msg.getMessage().getSequenceHeader().getRequestId() == requestId) + .check(reply -> sequenceValidator.test(reply.getMessage().getSequenceHeader(), future)) + .check(msg -> accumulateChunkUntilFinal(chunkStorage, msg.getChunk(), msg.getMessage())) + .unwrap(msg -> mergeChunks(chunkStorage, msg, msg.getMessage().getSequenceHeader(), chunkAssembler)) + .handle(response -> { + if (response.getChunk().equals(FINAL)) { + logger.debug("Received response made of {} bytes for message id: {}, channel id:{}, token:{}", + response.getLengthInBytes(), requestId, response.getSecurityHeader().getSecureChannelId(), + response.getSecurityHeader().getSecureTokenId() + ); + securityHeader.set(response.getSecurityHeader()); + + Payload message = response.getMessage(); + ExtensionObjectDefinition extensionObjectBody; + if (message instanceof ExtensiblePayload) { + extensionObjectBody = (((ExtensiblePayload) message).getPayload()).getBody(); + } else { + try { + BinaryPayload binary = (BinaryPayload) message; + ReadBufferByteBased buffer = new ReadBufferByteBased(binary.getPayload(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); + extensionObjectBody = ExtensionObject.staticParse(buffer, false).getBody(); + } catch (ParseException e) { + future.completeExceptionally(e); + return; + } + } + + if (extensionObjectBody instanceof ServiceFault) { + ServiceFault fault = (ServiceFault) extensionObjectBody; + future.completeExceptionally(toProtocolException(fault)); + } else { + future.complete(extensionObjectBody); + } + } + }); + + } else { + context.sendToWire(new OpcuaAPU(chunks.get(index))); + } + } + return future; + } + + private SendRequestContext sendRequest(MessagePDU messagePDU, CompletableFuture future, long timeout) { + return context.sendRequest(new OpcuaAPU(messagePDU)) + .onError((req, err) -> future.completeExceptionally(err)) + .expectResponse(OpcuaAPU.class, Duration.ofMillis(timeout)) + .onTimeout((e) -> future.completeExceptionally(e)); + } + + private T mergeChunks(ChunkStorage chunkStorage, T source, SequenceHeader sequenceHeader, BiFunction producer) { + byte[] message = chunkStorage.get(); + return producer.apply(source, + new BinaryPayload( + sequenceHeader, + message + ) + ); + } + + private boolean accumulateChunkUntilFinal(ChunkStorage storage, ChunkType chunkType, Payload data) { + if (ABORT.equals(chunkType)) { + storage.reset(); + return true; + } + + if (!(data instanceof BinaryPayload)) { + throw new IllegalArgumentException("Unexpected payload type " + data.getClass()); + } + storage.append(((BinaryPayload) data).getPayload()); + + return FINAL.equals(chunkType); + } + + // generate nonce used for setting up signing/encryption keys + private byte[] createNonce() { + return createNonce(securityPolicy.getNonceLength()); + } + + byte[] createNonce(int nonceLength) { + return RandomUtils.nextBytes(nonceLength); + } + + public boolean isSymmetricEncryptionEnabled() { + return messageSecurity == MessageSecurity.SIGN_ENCRYPT; + } + + public boolean isSymmetricSigningEnabled() { + return (messageSecurity == MessageSecurity.SIGN_ENCRYPT || messageSecurity == MessageSecurity.SIGN); + } + + static SecurityPolicy determineSecurityPolicy(OpcuaConfiguration configuration) { + if (configuration.isDiscovery() && configuration.getServerCertificate() == null) { + // discovery is enabled and sender certificate is not known yet + return SecurityPolicy.NONE; + } + + return configuration.getSecurityPolicy(); + } + + static PlcProtocolException toProtocolException(ServiceFault fault) { + if (fault.getResponseHeader() instanceof ResponseHeader) { + ResponseHeader responseHeader = (ResponseHeader) fault.getResponseHeader(); + long statusCode = responseHeader.getServiceResult().getStatusCode(); + String statusName = OpcuaStatusCode.isDefined(statusCode) ? OpcuaStatusCode.enumForValue(statusCode).name() : ""; + return new PlcProtocolException("Server returned error " + statusName + " (0x" + Long.toHexString(statusCode) + ")"); + } + return new PlcProtocolException("Unexpected service fault"); + } + + public OpcuaProtocolLimits getLimits() { + return limits; + } + + public byte[] getLocalNonce() { + return localNonce; + } + + public X509Certificate getLocalCertificate() { + return localCertificate; + } + + public void setRemoteNonce(byte[] remoteNonce) { + this.remoteNonce = remoteNonce; + } + + public byte[] getRemoteNonce() { + return remoteNonce; + } + + public X509Certificate getRemoteCertificate() { + return remoteCertificate; + } + + public SecurityPolicy getSecurityPolicy() { + return securityPolicy; + } + + public MessageSecurity getMessageSecurity() { + return messageSecurity; + } + + public byte[] encryptPassword(byte[] encodeablePassword) { + return encryptionHandler.encryptPassword(encodeablePassword); + } + + public void setSecurityHeader(SecurityHeader securityHeader) { + this.securityHeader.set(securityHeader); + } + + public SignatureData createClientSignature() throws GeneralSecurityException { + return encryptionHandler.createClientSignature(); + } + + public void setRemoteCertificate(X509Certificate certificate) { + this.remoteCertificate = certificate; + } + + public RequestHeader createRequestHeader(long requestTimeout) { + return createRequestHeader(requestTimeout, tm.getRequestHandle()); + } + + protected RequestHeader createRequestHeader(long requestTimeout, int requestHandle) { + return new RequestHeader( + new NodeId(authenticationToken.get()), + getCurrentDateTime(), + requestHandle, //RequestHandle + 0L, + SecureChannel.NULL_STRING, + requestTimeout, + NULL_EXTENSION_OBJECT + ); + } + + public RequestHeader createRequestHeader() { + return createRequestHeader(configuration.getRequestTimeout()); + } + + public static long getCurrentDateTime() { + return (System.currentTimeMillis() * 10000) + EPOCH_OFFSET; + } + + public void setAuthenticationToken(NodeIdTypeDefinition authenticationToken) { + this.authenticationToken.set(authenticationToken); + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java index a48940d6947..ee57d5e09ff 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java @@ -18,29 +18,24 @@ */ package org.apache.plc4x.java.opcua.context; -import static org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN; - import io.vavr.control.Try; -import java.io.ByteArrayInputStream; import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.Security; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; +import java.util.List; +import java.util.function.Supplier; import javax.crypto.Cipher; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.plc4x.java.opcua.protocol.OpcuaProtocolLogic; +import org.apache.plc4x.java.opcua.protocol.chunk.Chunk; +import org.apache.plc4x.java.opcua.protocol.chunk.ChunkFactory; import org.apache.plc4x.java.opcua.readwrite.MessagePDU; -import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU; import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenRequest; import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenResponse; +import org.apache.plc4x.java.opcua.readwrite.OpcuaProtocolLimits; import org.apache.plc4x.java.opcua.readwrite.PascalByteString; import org.apache.plc4x.java.opcua.readwrite.PascalString; import org.apache.plc4x.java.opcua.readwrite.SignatureData; import org.apache.plc4x.java.opcua.security.SecurityPolicy; -import org.apache.plc4x.java.spi.generation.ReadBuffer; -import org.apache.plc4x.java.spi.generation.ReadBufferByteBased; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,113 +43,79 @@ public class EncryptionHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaProtocolLogic.class); + private final Logger logger = LoggerFactory.getLogger(EncryptionHandler.class); static { // Required for SecurityPolicy.Aes128_Sha128_RsaPss Security.addProvider(new BouncyCastleProvider()); } + private final Conversation conversation; - private X509Certificate serverCertificate; - private X509Certificate clientCertificate; - private PrivateKey clientPrivateKey; - private PublicKey clientPublicKey; - private final SecurityPolicy securitypolicy; - - private byte[] clientNonce = null; - private byte[] serverNonce = null; private final SymmetricEncryptionHandler symmetricEncryptionHandler; private final AsymmetricEncryptionHandler asymmetricEncryptionHandler; - public EncryptionHandler(CertificateKeyPair ckp, byte[] senderCertificate, SecurityPolicy securityPolicy) { - if (ckp != null) { - this.clientPrivateKey = ckp.getKeyPair().getPrivate(); - this.clientPublicKey = ckp.getKeyPair().getPublic(); - this.clientCertificate = ckp.getCertificate(); - } - if (senderCertificate != null) { - this.serverCertificate = getCertificateX509(senderCertificate); - } - this.securitypolicy = securityPolicy; - this.symmetricEncryptionHandler = new SymmetricEncryptionHandler(securityPolicy); - this.asymmetricEncryptionHandler = new AsymmetricEncryptionHandler(serverCertificate, clientCertificate, clientPrivateKey, clientPublicKey, securitypolicy); + public EncryptionHandler(Conversation conversation, PrivateKey senderPrivateKey) { + this.conversation = conversation; + this.symmetricEncryptionHandler = new SymmetricEncryptionHandler(conversation, conversation.getSecurityPolicy()); + this.asymmetricEncryptionHandler = new AsymmetricEncryptionHandler(conversation, conversation.getSecurityPolicy(), senderPrivateKey); } - public void setServerCertificate(X509Certificate serverCertificate) { - this.serverCertificate = serverCertificate; - } + public List encodeMessage(MessagePDU message, Supplier sequenceSupplier) { + OpcuaProtocolLimits limits = conversation.getLimits(); + logger.debug("Encoding Message with Security policy {} and encoding limits {}", conversation.getSecurityPolicy(), limits); - public ReadBuffer encodeMessage(MessagePDU pdu, byte[] message) { - switch (securitypolicy) { - case NONE: - return new ReadBufferByteBased(message, LITTLE_ENDIAN); - case Basic256Sha256: - case Basic128Rsa15: - if (pdu instanceof OpcuaOpenRequest) { - return asymmetricEncryptionHandler.encodeMessage(pdu, message); - } else { - return symmetricEncryptionHandler.encodeMessage(pdu, message, clientNonce, serverNonce); - } - default: - throw new IllegalStateException("Driver doesn't support security policy: " + securitypolicy); + if (message instanceof OpcuaOpenRequest || message instanceof OpcuaOpenResponse) { + Chunk chunk = new ChunkFactory().create(true, conversation.isSymmetricEncryptionEnabled(), conversation.isSymmetricSigningEnabled(), + conversation.getSecurityPolicy(), limits, + conversation.getLocalCertificate(), conversation.getRemoteCertificate() + ); + return asymmetricEncryptionHandler.encodeMessage(chunk, message, sequenceSupplier); } + + Chunk chunk = new ChunkFactory().create(false, conversation.isSymmetricEncryptionEnabled(), conversation.isSymmetricSigningEnabled(), + conversation.getSecurityPolicy(), limits, + conversation.getLocalCertificate(), conversation.getRemoteCertificate() + ); + return symmetricEncryptionHandler.encodeMessage(chunk, message, sequenceSupplier); } + public MessagePDU decodeMessage(MessagePDU message) { + OpcuaProtocolLimits limits = conversation.getLimits(); + logger.debug("Decoding Message with Security policy {} and encoding limits {}", conversation.getSecurityPolicy(), limits); - public SignatureData createClientSignature(byte[] lastServerNonce) { - byte[] cert = Try.of(() -> serverCertificate.getEncoded()).getOrElse(new byte[0]); - byte[] bytes = ByteBuffer.allocate(cert.length+lastServerNonce.length).put(cert).put(lastServerNonce).array(); - byte[] signed = asymmetricEncryptionHandler.sign(bytes); - return new SignatureData(new PascalString(securitypolicy.getAsymmetricSignatureAlgorithm().getUri()), new PascalByteString(signed.length, signed)); + if (message instanceof OpcuaOpenResponse || message instanceof OpcuaOpenRequest) { + Chunk chunk = new ChunkFactory().create(true, conversation.isSymmetricEncryptionEnabled(), conversation.isSymmetricSigningEnabled(), + conversation.getSecurityPolicy(), limits, + conversation.getRemoteCertificate(), conversation.getLocalCertificate() + ); + return asymmetricEncryptionHandler.decodeMessage(chunk, message); + } + Chunk chunk = new ChunkFactory().create(false, conversation.isSymmetricEncryptionEnabled(), conversation.isSymmetricSigningEnabled(), + conversation.getSecurityPolicy(), limits, + conversation.getRemoteCertificate(), conversation.getLocalCertificate() + ); + return symmetricEncryptionHandler.decodeMessage(chunk, message); } - public OpcuaAPU decodeMessage(OpcuaAPU pdu) { - LOGGER.info("Decoding Message with Security policy {}", securitypolicy); - - switch (securitypolicy) { - case NONE: - return pdu; - case Basic128Rsa15: - case Basic256Sha256: - if (pdu.getMessage() instanceof OpcuaOpenResponse) { - return asymmetricEncryptionHandler.decodeMessage(pdu); - } else { - return symmetricEncryptionHandler.decodeMessage(pdu, clientNonce, serverNonce); - } - default: - throw new IllegalStateException("Driver doesn't support security policy: " + securitypolicy); - } + public SignatureData createClientSignature() throws GeneralSecurityException { + SecurityPolicy securityPolicy = conversation.getSecurityPolicy(); + byte[] lastServerNonce = conversation.getRemoteNonce(); + byte[] cert = Try.of(() -> conversation.getRemoteCertificate().getEncoded()).getOrElse(new byte[0]); + byte[] bytes = ByteBuffer.allocate(cert.length + lastServerNonce.length).put(cert).put(lastServerNonce).array(); + byte[] signed = asymmetricEncryptionHandler.sign(bytes); + return new SignatureData(new PascalString(securityPolicy.getAsymmetricSignatureAlgorithm().getUri()), new PascalByteString(signed.length, signed)); } public byte[] encryptPassword(byte[] data) { try { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, this.serverCertificate.getPublicKey()); + cipher.init(Cipher.ENCRYPT_MODE, this.conversation.getRemoteCertificate().getPublicKey()); return cipher.doFinal(data); } catch (Exception e) { - LOGGER.error("Unable to encrypt Data", e); + logger.error("Unable to encrypt Data", e); return null; } } - public static X509Certificate getCertificateX509(byte[] senderCertificate) { - try { - CertificateFactory factory = CertificateFactory.getInstance("X.509"); - LOGGER.info("Public Key Length {}", senderCertificate.length); - return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(senderCertificate)); - } catch (Exception e) { - LOGGER.error("Unable to get certificate from String {}", senderCertificate); - return null; - } - } - - public void setClientNonce(byte[] clientNonce) { - this.clientNonce = clientNonce; - } - - - public void setServerNonce(byte[] serverNonce) { - this.serverNonce = serverNonce; - } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java index 9d7c75e5f7a..3bc503f4eec 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java @@ -20,6 +20,7 @@ package org.apache.plc4x.java.opcua.context; import java.util.Optional; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; import org.apache.plc4x.java.opcua.config.OpcuaConfiguration; import org.apache.plc4x.java.opcua.readwrite.PascalByteString; @@ -68,9 +69,9 @@ public class OpcuaDriverContext implements DriverContext, HasConfiguration getApplicationUri() { .flatMap(CertificateKeyPair::getApplicationUri); } + public PascalByteString getThumbprint() { + return thumbprint; + } } 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 b8972c77a11..f7c6216b715 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 @@ -3,7 +3,7 @@ * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the + * to you under the Apache License, PROTOCOL_VERSION_0 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 * @@ -18,26 +18,32 @@ */ package org.apache.plc4x.java.opcua.context; -import io.vavr.control.Try; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; +import static org.apache.plc4x.java.opcua.readwrite.ChunkType.*; -import static java.lang.Thread.currentThread; -import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static java.util.concurrent.ForkJoinPool.commonPool; - -import java.time.Instant; +import java.io.ByteArrayInputStream; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.Supplier; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.apache.plc4x.java.api.authentication.PlcAuthentication; import org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication; -import org.apache.plc4x.java.api.exceptions.PlcConnectionException; import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; import org.apache.plc4x.java.opcua.config.OpcuaConfiguration; import org.apache.plc4x.java.opcua.readwrite.*; import org.apache.plc4x.java.opcua.security.SecurityPolicy; -import org.apache.plc4x.java.spi.ConversationContext; +import org.apache.plc4x.java.opcua.security.SecurityPolicy.SignatureAlgorithm; import org.apache.plc4x.java.spi.generation.*; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager.RequestTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,18 +51,10 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.MessageDigest; -import java.security.cert.CertificateEncodingException; -import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -64,32 +62,9 @@ public class SecureChannel { private static final Logger LOGGER = LoggerFactory.getLogger(SecureChannel.class); - private static final String FINAL_CHUNK = "F"; - private static final String CONTINUATION_CHUNK = "C"; - private static final String ABORT_CHUNK = "A"; - private static final int VERSION = 0; - 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; - public static final Duration REQUEST_TIMEOUT = Duration.ofMillis(1000000); - public static final long REQUEST_TIMEOUT_LONG = 1000000L; private static final String PASSWORD_ENCRYPTION_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#rsa-oaep"; - private static final PascalString SECURITY_POLICY_NONE = new PascalString("http://opcfoundation.org/UA/SecurityPolicy#None"); - protected static final PascalString NULL_STRING = new PascalString(""); - private static final PascalByteString NULL_BYTE_STRING = new PascalByteString(-1, null); - private static final ExpandedNodeId NULL_EXPANDED_NODE_ID = new ExpandedNodeId(false, - false, - new NodeIdTwoByte((short) 0), - null, - null - ); - - protected static final ExtensionObject NULL_EXTENSION_OBJECT = new ExtensionObject( - NULL_EXPANDED_NODE_ID, - new ExtensionObjectEncodingMask(false, false, false), - new NullExtension()); // Body - + public static final PascalString NULL_STRING = new PascalString(""); + public static final PascalByteString NULL_BYTE_STRING = new PascalByteString(-1, null); public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("(.(?tcp))?://" + "(?[\\w.-]+)(:" + "(?\\d*))?"); @@ -99,43 +74,31 @@ public class SecureChannel { "(?[\\w/=]*)[?]?" ); - private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time. private static final PascalString APPLICATION_URI = new PascalString("urn:apache:plc4x:client"); private static final PascalString PRODUCT_URI = new PascalString("urn:apache:plc4x:client"); private static final PascalString APPLICATION_TEXT = new PascalString("OPCUA client for the Apache PLC4X:PLC4J project"); - private static final long DEFAULT_CONNECTION_LIFETIME = 36000000; + public static final ScheduledExecutorService KEEP_ALIVE_EXECUTOR = newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "plc4x-opcua-keep-alive")); private final String sessionName = "UaSession:" + APPLICATION_TEXT.getStringValue() + ":" + RandomStringUtils.random(20, true, true); - private final byte[] clientNonce = RandomUtils.nextBytes(40); - private final AtomicInteger requestHandleGenerator = new AtomicInteger(1); + private final PascalByteString localCertificateString; + private final PascalByteString remoteCertificateThumbprint; private PascalString policyId; private UserTokenType tokenType; private final PascalString endpoint; private final String username; private final String password; - private final SecurityPolicy securityPolicy; - private final PascalByteString publicCertificate; - private final PascalByteString thumbprint; - private final boolean isEncrypted; - private byte[] senderCertificate = null; - private byte[] senderNonce = null; - private EncryptionHandler encryptionHandler; + private final RequestTransactionManager tm; private final OpcuaConfiguration configuration; private final OpcuaDriverContext driverContext; - private final AtomicInteger channelId = new AtomicInteger(1); - private final AtomicInteger tokenId = new AtomicInteger(1); - private NodeIdTypeDefinition authenticationToken = new NodeIdTwoByte((short) 0); - private ConversationContext context; - private final SecureChannelTransactionManager channelTransactionManager = new SecureChannelTransactionManager(); - private long lifetime = DEFAULT_CONNECTION_LIFETIME; - private CompletableFuture keepAlive; + private Conversation conversation; + private ScheduledFuture keepAlive; private final List endpoints = new ArrayList<>(); - private final AtomicLong senderSequenceNumber = new AtomicLong(); - private final AtomicBoolean enableKeepalive = new AtomicBoolean(true); - private double sessionTimeout = 120000L; + private double sessionTimeout; + private long revisedLifetime; - public SecureChannel(OpcuaDriverContext driverContext, OpcuaConfiguration configuration, PlcAuthentication authentication) { + public SecureChannel(Conversation conversation, RequestTransactionManager tm, OpcuaDriverContext driverContext, OpcuaConfiguration configuration, PlcAuthentication authentication) { + this.conversation = conversation; + this.tm = tm; this.configuration = configuration; - this.driverContext = driverContext; this.endpoint = new PascalString(driverContext.getEndpoint()); if (authentication != null) { @@ -149,27 +112,6 @@ public SecureChannel(OpcuaDriverContext driverContext, OpcuaConfiguration config this.username = configuration.getUsername(); this.password = configuration.getPassword(); } - this.securityPolicy = determineSecurityPolicy(configuration, driverContext); - CertificateKeyPair ckp = driverContext.getCertificateKeyPair(); - - if (this.securityPolicy != SecurityPolicy.NONE) { - //Sender Certificate gets populated during the 'discover' phase when encryption is enabled. - this.senderCertificate = configuration.getSenderCertificate(); - this.encryptionHandler = new EncryptionHandler(ckp, this.senderCertificate, configuration.getSecurityPolicy()); - try { - this.publicCertificate = new PascalByteString(ckp.getCertificate().getEncoded().length, ckp.getCertificate().getEncoded()); - this.isEncrypted = true; - } catch (CertificateEncodingException e) { - throw new PlcRuntimeException("Failed to encode the certificate"); - } - this.thumbprint = configuration.getThumbprint(); - } else { - this.encryptionHandler = new EncryptionHandler(ckp, this.senderCertificate, configuration.getSecurityPolicy()); - this.publicCertificate = NULL_BYTE_STRING; - this.thumbprint = NULL_BYTE_STRING; - this.isEncrypted = false; - } - encryptionHandler.setClientNonce(clientNonce); // Generate a list of endpoints we can use. try { @@ -181,239 +123,102 @@ public SecureChannel(OpcuaDriverContext driverContext, OpcuaConfiguration config LOGGER.warn("Unable to resolve host name. Using original host from connection string which may cause issues connecting to server"); this.endpoints.add(driverContext.getHost()); } - } - - private SecurityPolicy determineSecurityPolicy(OpcuaConfiguration configuration, OpcuaDriverContext driverContext) { - if (configuration.isDiscovery() && configuration.getSenderCertificate() == null) { - // discovery is enabled and sender certificate is not known yet - return SecurityPolicy.NONE; - } - return configuration.getSecurityPolicy(); - } - - public synchronized void submit(ConversationContext context, Consumer onTimeout, BiConsumer error, Consumer consumer, WriteBufferByteBased buffer) { - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - //TODO: We need to split large messages up into chunks if it is larger than the sendBufferSize - // This value is negotiated when opening a channel - - OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK, - channelId.get(), - tokenId.get(), - transactionId, - transactionId, - buffer.getBytes()); - - final OpcuaAPU apu; - try { - if (this.isEncrypted) { - encryptionHandler.setServerNonce(senderNonce); - apu = OpcuaAPU.staticParse(encryptionHandler.encodeMessage(messageRequest, buffer.getBytes()), false); - } else { - apu = new OpcuaAPU(messageRequest); - } - } catch (ParseException e) { - throw new PlcRuntimeException("Unable to encrypt message before sending"); - } - - Consumer requestConsumer = t -> { + if (conversation.getSecurityPolicy() == SecurityPolicy.NONE) { + this.localCertificateString = NULL_BYTE_STRING; + this.remoteCertificateThumbprint = NULL_BYTE_STRING; + } else { + CertificateKeyPair keyPair = driverContext.getCertificateKeyPair(); + this.remoteCertificateThumbprint = driverContext.getThumbprint(); try { - ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream(); - context.sendRequest(apu) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .onTimeout(onTimeout) - .onError(error) - .unwrap(encryptionHandler::decodeMessage) - .unwrap(OpcuaAPU::getMessage) - .check(OpcuaMessageResponse.class::isInstance) - .unwrap(OpcuaMessageResponse.class::cast) - .check(p -> p.getRequestId() == transactionId) - .check(p -> accumulate(chunkStorage, p)) - .unwrap(p -> mergeChunks(chunkStorage, p)) -// .check(p -> { -// if (p.getRequestId() == transactionId) { -// try { -// messageBuffer.write(p.getMessage()); -// if (!(senderSequenceNumber.incrementAndGet() == (p.getSequenceNumber()))) { -// LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - {} != {}", senderSequenceNumber.incrementAndGet(), p.getSequenceNumber()); -// context.fireDisconnected(); -// } -// } catch (IOException e) { -// LOGGER.debug("Failed to store incoming message in buffer"); -// throw new PlcRuntimeException("Error while sending message"); -// } -// return p.getChunk().equals(FINAL.getValue()); -// } else { -// return false; -// } -// }) - .handle(opcuaResponse -> { - if (opcuaResponse.getChunk().equals(FINAL_CHUNK)) { - tokenId.set(opcuaResponse.getSecureTokenId()); - channelId.set(opcuaResponse.getSecureChannelId()); - - dispatch(() -> consumer.accept(opcuaResponse.getMessage())); - } - }); - } catch (Exception e) { - throw new PlcRuntimeException("Error while sending message"); + byte[] encoded = keyPair.getCertificate().getEncoded(); + this.localCertificateString = new PascalByteString(encoded.length, encoded); + } catch (CertificateEncodingException e) { + throw new PlcRuntimeException("Could not decode certificate", e); } - }; - LOGGER.debug("Submitting Transaction to TransactionManager {}", transactionId); - channelTransactionManager.submit(requestConsumer, transactionId); + } } - public void onConnect(ConversationContext context) { + public CompletableFuture onConnect() { // Only the TCP transport supports login. LOGGER.debug("Opcua Driver running in ACTIVE mode."); - this.context = context; - - OpcuaHelloRequest hello = new OpcuaHelloRequest( - FINAL_CHUNK, - VERSION, - DEFAULT_RECEIVE_BUFFER_SIZE, - DEFAULT_SEND_BUFFER_SIZE, - DEFAULT_MAX_MESSAGE_SIZE, - DEFAULT_MAX_CHUNK_COUNT, - this.endpoint - ); - - Consumer requestConsumer = t -> context - .sendRequest(new OpcuaAPU(hello)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse) - .unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage()) - .handle(opcuaAcknowledgeResponse -> commonPool().submit(() -> onConnectOpenSecureChannel(context, opcuaAcknowledgeResponse))); - channelTransactionManager.submit(requestConsumer, channelTransactionManager.getTransactionIdentifier()); + return conversation.requestHello() + .thenCompose(r -> onConnectOpenSecureChannel(SecurityTokenRequestType.securityTokenRequestTypeIssue)) + .thenCompose(r -> onConnectCreateSessionRequest(r)) + .thenCompose(r -> onConnectActivateSessionRequest(r)) + .thenApply(response -> { + keepAlive(); + return response; + }); } - public void onConnectOpenSecureChannel(ConversationContext context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) { - int transactionId = channelTransactionManager.getTransactionIdentifier(); + public CompletableFuture onConnectOpenSecureChannel(SecurityTokenRequestType securityTokenRequestType) { + LOGGER.debug("Sending open secure channel message to {}", this.driverContext.getEndpoint()); - RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, //RequestHandle - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); + RequestHeader requestHeader = conversation.createRequestHeader(configuration.getOpenChannelTimeout(), 0); OpenSecureChannelRequest openSecureChannelRequest; - if (this.isEncrypted) { + if (conversation.getSecurityPolicy() != SecurityPolicy.NONE) { + byte[] localNonce = conversation.getLocalNonce(); openSecureChannelRequest = new OpenSecureChannelRequest( requestHeader, - VERSION, - SecurityTokenRequestType.securityTokenRequestTypeIssue, - MessageSecurityMode.messageSecurityModeSignAndEncrypt, - new PascalByteString(clientNonce.length, clientNonce), - lifetime + OpcuaConstants.PROTOCOLVERSION, + securityTokenRequestType, + configuration.getMessageSecurity().getMode(), + new PascalByteString(localNonce.length, localNonce), + configuration.getChannelLifetime() // lifetime ); } else { openSecureChannelRequest = new OpenSecureChannelRequest( requestHeader, - VERSION, - SecurityTokenRequestType.securityTokenRequestTypeIssue, + OpcuaConstants.PROTOCOLVERSION, + securityTokenRequestType, MessageSecurityMode.messageSecurityModeNone, NULL_BYTE_STRING, - lifetime + configuration.getChannelLifetime() // lifetime ); } - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte( - (short) 0, Integer.parseInt(openSecureChannelRequest.getIdentifier()) - ), - null, - null - ); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - openSecureChannelRequest + ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, false, + new NodeIdFourByte((short) 0, Integer.parseInt(openSecureChannelRequest.getIdentifier())), + null, null ); + ExtensionObject extObject = new ExtensionObject(expandedNodeId, null, openSecureChannelRequest); - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - OpcuaOpenRequest openRequest = new OpcuaOpenRequest( - FINAL_CHUNK, + Function openRequest = context -> { + LOGGER.debug("Submitting OpenSecureChannel with id of {}", context.getRequestId()); + return new OpcuaOpenRequest(FINAL, new OpenChannelMessageRequest( 0, - new PascalString(this.securityPolicy.getSecurityPolicyUri()), - this.publicCertificate, - this.thumbprint, - requestId, - requestId, - buffer.getBytes() - ); - - final OpcuaAPU apu; - - if (this.isEncrypted) { - apu = OpcuaAPU.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getBytes()), false); - } else { - apu = new OpcuaAPU(openRequest); - } + new PascalString(conversation.getSecurityPolicy().getSecurityPolicyUri()), + this.localCertificateString, + this.remoteCertificateThumbprint + ), + new ExtensiblePayload( + new SequenceHeader(context.getNextSequenceNumber(), context.getRequestId()), + extObject + )); + }; - Consumer requestConsumer = t -> context.sendRequest(apu) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage)) - .check(p -> p.getMessage() instanceof OpcuaOpenResponse) - .unwrap(p -> (OpcuaOpenResponse) p.getMessage()) - .check(p -> p.getRequestId() == transactionId) - .handle(opcuaOpenResponse -> { - try { - ReadBuffer readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - ExtensionObject message = ExtensionObject.staticParse(readBuffer, false); - //Store the initial sequence number from the server. there's no requirement for the server and client to use the same starting number. - senderSequenceNumber.set(opcuaOpenResponse.getSequenceNumber()); - - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - } else { - LOGGER.debug("Got answer for open secure channel request"); - OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody(); - ChannelSecurityToken securityToken = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken(); - tokenId.set((int) securityToken.getTokenId()); - channelId.set((int) securityToken.getChannelId()); - lifetime = securityToken.getRevisedLifetime(); - this.senderNonce = openSecureChannelResponse.getServerNonce().getStringValue(); - this.encryptionHandler.setServerNonce(openSecureChannelResponse.getServerNonce().getStringValue()); - commonPool().submit(() -> { - try { - onConnectCreateSessionRequest(context); - } catch (PlcConnectionException e) { - LOGGER.error("Error occurred while connecting to OPC UA server", e); - } - }); - } - } catch (ParseException e) { - LOGGER.error("Error parsing", e); - } - }); - LOGGER.debug("Submitting OpenSecureChannel with id of {}", transactionId); - channelTransactionManager.submit(requestConsumer, transactionId); - } catch (SerializationException | ParseException e) { - LOGGER.error("Unable to to Parse Open Secure Request"); - } + return conversation.requestChannelOpen(openRequest) + .thenApply(response -> { + LOGGER.info("Received open channel response {}, parsing it", response.getMessage().getSequenceHeader().getRequestId()); + return response; + }) + .thenApply(this::onOpenResponse) + .thenApply(openSecureChannelResponse -> { + ChannelSecurityToken securityToken = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken(); + LOGGER.debug("Opened secure response id: {}, channel id:{}, token:{} lifetime:{}", openSecureChannelResponse.getIdentifier(), + securityToken.getChannelId(), securityToken.getTokenId(), securityToken.getRevisedLifetime()); + + conversation.setSecurityHeader(new SecurityHeader(securityToken.getChannelId(), securityToken.getTokenId())); + revisedLifetime = securityToken.getRevisedLifetime(); + return openSecureChannelResponse; + }); } - public void onConnectCreateSessionRequest(ConversationContext context) throws PlcConnectionException { - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); + public CompletableFuture onConnectCreateSessionRequest(OpenSecureChannelResponse response) { + LOGGER.debug("Sending create session request to {}", this.driverContext.getEndpoint()); + RequestHeader requestHeader = conversation.createRequestHeader(); LocalizedText applicationName = new LocalizedText( true, @@ -436,88 +241,78 @@ public void onConnectCreateSessionRequest(ConversationContext context) discoveryUrls ); + ChannelSecurityToken securityToken = (ChannelSecurityToken) response.getSecurityToken(); + LOGGER.debug("Opened secure response id: {}, channel id:{}, token:{} lifetime:{}", response.getIdentifier(), + securityToken.getChannelId(), securityToken.getTokenId(), securityToken.getRevisedLifetime()); + conversation.setRemoteNonce(response.getServerNonce().getStringValue()); + byte[] temporaryNonce = conversation.createNonce(32); CreateSessionRequest createSessionRequest = new CreateSessionRequest( requestHeader, clientDescription, NULL_STRING, this.endpoint, new PascalString(sessionName), - new PascalByteString(clientNonce.length, clientNonce), - securityPolicy == SecurityPolicy.NONE ? NULL_BYTE_STRING : publicCertificate, + conversation.getSecurityPolicy() == SecurityPolicy.NONE ? NULL_BYTE_STRING : createPascalString(temporaryNonce), + conversation.getSecurityPolicy() == SecurityPolicy.NONE ? NULL_BYTE_STRING : localCertificateString, sessionTimeout, 0L ); - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(createSessionRequest.getIdentifier())), - null, - null - ); + return conversation.submit(createSessionRequest, CreateSessionResponse.class) + .thenApply(sessionResponse -> { + if (conversation.getSecurityPolicy() != SecurityPolicy.NONE) { + // verify temporaryNonce against server returned data + SignatureData signatureData = extractSignatureData(sessionResponse.getServerSignature()); + if (signatureData == null) { + throw new IllegalArgumentException("Returned signature data is not valid"); + } - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - createSessionRequest); + String algorithm = signatureData.getAlgorithm().getStringValue(); - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - Consumer consumer = opcuaResponse -> { - try { - ExtensionObject message = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false); - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - } else { - LOGGER.debug("Got Create Session Response Connection Response"); - try { - CreateSessionResponse responseMessage; - - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof CreateSessionResponse) { - responseMessage = (CreateSessionResponse) unknownExtensionObject; - - authenticationToken = responseMessage.getAuthenticationToken().getNodeId(); - sessionTimeout = responseMessage.getRevisedSessionTimeout(); - - onConnectActivateSessionRequest(context, responseMessage, (CreateSessionResponse) message.getBody()); - } else { - ServiceFault serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString()); - } - - } catch (PlcConnectionException e) { - LOGGER.error("Error occurred while connecting to OPC UA server"); - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Subscription response", e); + SignatureAlgorithm signatureAlgorithm = conversation.getSecurityPolicy().getAsymmetricSignatureAlgorithm(); + if (!signatureAlgorithm.getUri().equals(algorithm)) { + throw new IllegalArgumentException("Invalid signature algorithm. Expected " + signatureAlgorithm.getUri()); + } + try { + int certificateLength = localCertificateString.getStringLength(); + byte[] rawData = new byte[certificateLength + 32]; + System.arraycopy(localCertificateString.getStringValue(), 0, rawData, 0, certificateLength); + System.arraycopy(temporaryNonce, 0, rawData, certificateLength, 32); + X509Certificate remoteCertificate = conversation.getRemoteCertificate(); + + Signature signature = signatureAlgorithm.getSignature(); + signature.initVerify(remoteCertificate.getPublicKey()); + signature.update(rawData); + if (!signature.verify(signatureData.getSignature().getStringValue())) { + throw new IllegalArgumentException("Could not verify server signature"); } + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); } - } catch (ParseException e) { - LOGGER.error("Error parsing", e); - } - }; - - Consumer timeout = e -> { - LOGGER.error("Timeout while waiting for subscription response", e); - }; - BiConsumer error = (message, e) -> LOGGER.error("Error while waiting for subscription response", e); + } + return sessionResponse; + }) + .thenApply(responseMessage -> { + conversation.setAuthenticationToken(responseMessage.getAuthenticationToken().getNodeId()); + sessionTimeout = responseMessage.getRevisedSessionTimeout(); + return responseMessage; + }); + } - submit(context, timeout, error, consumer, buffer); - } catch (SerializationException e) { - LOGGER.error("Unable to to Parse Create Session Request"); + private SignatureData extractSignatureData(ExtensionObjectDefinition object) { + if (object instanceof SignatureData) { + return (SignatureData) object; } + return null; } - private void onConnectActivateSessionRequest(ConversationContext context, CreateSessionResponse opcuaMessageResponse, CreateSessionResponse sessionResponse) throws PlcConnectionException, ParseException { - senderCertificate = sessionResponse.getServerCertificate().getStringValue(); - encryptionHandler.setServerCertificate(EncryptionHandler.getCertificateX509(senderCertificate)); - this.senderNonce = sessionResponse.getServerNonce().getStringValue(); + private CompletableFuture onConnectActivateSessionRequest(CreateSessionResponse sessionResponse) { + LOGGER.debug("Sending activate session request to {}", this.driverContext.getEndpoint()); + conversation.setRemoteCertificate(getX509Certificate(sessionResponse.getServerCertificate().getStringValue())); + conversation.setRemoteNonce(sessionResponse.getServerNonce().getStringValue()); + String[] endpoints = new String[3]; try { InetAddress address = InetAddress.getByName(driverContext.getHost()); @@ -535,21 +330,15 @@ private void onConnectActivateSessionRequest(ConversationContext conte } ExtensionObject userIdentityToken = getIdentityToken(this.tokenType, policyId.getStringValue()); - - int requestHandle = getRequestHandle(); - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - requestHandle, - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); - - SignatureData emptySignature = new SignatureData(NULL_STRING, NULL_BYTE_STRING); - SignatureData clientSignature = securityPolicy == SecurityPolicy.NONE ? emptySignature : encryptionHandler.createClientSignature(this.senderNonce); + RequestHeader requestHeader = conversation.createRequestHeader(); + SignatureData clientSignature = new SignatureData(NULL_STRING, NULL_BYTE_STRING); + if (conversation.getSecurityPolicy() != SecurityPolicy.NONE) { + try { + clientSignature = conversation.createClientSignature(); + } catch (GeneralSecurityException e) { + throw new PlcRuntimeException("Could not create client signature", e); + } + } ActivateSessionRequest activateSessionRequest = new ActivateSessionRequest( requestHeader, @@ -562,343 +351,66 @@ private void onConnectActivateSessionRequest(ConversationContext conte clientSignature ); - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(activateSessionRequest.getIdentifier())), - null, - null - ); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - activateSessionRequest - ); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - Consumer consumer = opcuaResponse -> { - try { - ExtensionObject message = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false); - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - return; - } - } catch (ParseException e) { - LOGGER.error("Error parsing", e); - return; - } - LOGGER.debug("Got Activate Session Response Connection Response"); - try { - ActivateSessionResponse responseMessage; - - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof ActivateSessionResponse) { - responseMessage = (ActivateSessionResponse) unknownExtensionObject; - - long returnedRequestHandle = ((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle(); - if (!(requestHandle == returnedRequestHandle)) { - LOGGER.error("Request handle isn't as expected, we might have missed a packet. {} != {}", requestHandle, returnedRequestHandle); - } - - // Send an event that connection setup is complete. - keepAlive(); - context.fireConnected(); - } else { - ServiceFault serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString()); - } - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Subscription response", e); - } - }; - - Consumer timeout = e -> LOGGER.error("Timeout while waiting for activate session response", e); - - BiConsumer error = (message, e) -> LOGGER.error("Error while waiting for activate session response", e); - - submit(context, timeout, error, consumer, buffer); - } catch (SerializationException e) { - LOGGER.error("Unable to to Parse Activate Session Request", e); - } + return conversation.submit(activateSessionRequest, ActivateSessionResponse.class).thenApply(responseMessage -> { + conversation.setRemoteNonce(responseMessage.getServerNonce().getStringValue()); + return responseMessage; + }); } - public void onDisconnect(ConversationContext context) { + public void onDisconnect() { LOGGER.info("Disconnecting"); - int requestHandle = getRequestHandle(); if (keepAlive != null) { - enableKeepalive.set(false); + keepAlive.cancel(true); + keepAlive = null; } - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, 473), - null, - null - ); //Identifier for OpenSecureChannel - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - requestHandle, //RequestHandle - 0L, - NULL_STRING, - 5000L, - NULL_EXTENSION_OBJECT - ); - - CloseSessionRequest closeSessionRequest = new CloseSessionRequest( - requestHeader, - true - ); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - closeSessionRequest - ); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - Consumer consumer = opcuaResponse -> { - try { - ExtensionObject message = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false); - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - return; - } - } catch (ParseException e) { - LOGGER.error("Error parsing", e); - } - LOGGER.debug("Got Close Session Response Connection Response"); - try { - CloseSessionResponse responseMessage; - - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof CloseSessionResponse) { - responseMessage = (CloseSessionResponse) unknownExtensionObject; - - LOGGER.trace("Got Close Session Response Connection Response" + responseMessage); - onDisconnectCloseSecureChannel(context); - } else { - ServiceFault serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString()); - } - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Close Session response", e); - } - - }; - - Consumer timeout = e -> LOGGER.error("Timeout while waiting for close session response", e); - - BiConsumer error = (message, e) -> LOGGER.error("Error while waiting for close session response", e); - - submit(context, timeout, error, consumer, buffer); - } catch (SerializationException e) { - LOGGER.error("Unable to to Parse Close Session Request", e); - } + RequestHeader requestHeader = conversation.createRequestHeader(50000L); + CloseSessionRequest closeSessionRequest = new CloseSessionRequest(requestHeader, true); + conversation.submit(closeSessionRequest, CloseSessionResponse.class).thenAccept(responseMessage -> { + LOGGER.trace("Got Close Session Response Connection Response" + responseMessage); + onDisconnectCloseSecureChannel(); + }); } - private void onDisconnectCloseSecureChannel(ConversationContext context) { - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, //RequestHandle - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); - + private void onDisconnectCloseSecureChannel() { + RequestHeader requestHeader = conversation.createRequestHeader(); CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader); - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified + ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, false, new NodeIdFourByte((short) 0, Integer.parseInt(closeSecureChannelRequest.getIdentifier())), - null, - null + null, null ); - OpcuaCloseRequest closeRequest = new OpcuaCloseRequest( - FINAL_CHUNK, - channelId.get(), - tokenId.get(), - transactionId, - transactionId, - new ExtensionObject( - expandedNodeId, - null, - closeSecureChannelRequest + Function closeRequest = ctx -> + new OpcuaCloseRequest(FINAL, ctx.getSecurityHeader(), + new ExtensiblePayload( + new SequenceHeader(ctx.getNextSequenceNumber(), ctx.getRequestId()), + new ExtensionObject(expandedNodeId, null, closeSecureChannelRequest) ) ); - Consumer requestConsumer = t -> { - context.sendRequest(new OpcuaAPU(closeRequest)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaMessageResponse) - .unwrap(p -> (OpcuaMessageResponse) p.getMessage()) - .check(p -> p.getRequestId() == transactionId) - .handle(opcuaMessageResponse -> LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString())); - - context.fireDisconnected(); - }; - - channelTransactionManager.submit(requestConsumer, transactionId); + conversation.requestChannelClose(closeRequest); } - public void onDiscover(ConversationContext context) { - if (!driverContext.getEncrypted()) { - LOGGER.debug("not encrypted, ignoring onDiscover"); - context.fireDiscovered(this.configuration); - return; - } + public CompletableFuture onDiscover() { // Only the TCP transport supports login. LOGGER.debug("Opcua Driver running in ACTIVE mode, discovering endpoints"); - OpcuaHelloRequest hello = new OpcuaHelloRequest(FINAL_CHUNK, - VERSION, - DEFAULT_RECEIVE_BUFFER_SIZE, - DEFAULT_SEND_BUFFER_SIZE, - DEFAULT_MAX_MESSAGE_SIZE, - DEFAULT_MAX_CHUNK_COUNT, - this.endpoint); - - Consumer requestConsumer = t -> context.sendRequest(new OpcuaAPU(hello)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse) - .unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage()) - .handle(opcuaAcknowledgeResponse -> { - LOGGER.debug("Got Hello Response Connection Response"); - commonPool().submit(() -> onDiscoverOpenSecureChannel(context, opcuaAcknowledgeResponse)); + return conversation.requestHello() + .thenCompose(ack -> onConnectOpenSecureChannel(SecurityTokenRequestType.securityTokenRequestTypeIssue)) + .thenCompose(scr -> onDiscoverGetEndpointsRequest(scr)) + .thenApply(endpoint -> { + LOGGER.info("Finished discovery of communication endpoint"); + return endpoint; }); - - channelTransactionManager.submit(requestConsumer, channelTransactionManager.getTransactionIdentifier()); - } - - - public void onDiscoverOpenSecureChannel(ConversationContext context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) { - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, //RequestHandle - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); - - OpenSecureChannelRequest openSecureChannelRequest = new OpenSecureChannelRequest( - requestHeader, - VERSION, - SecurityTokenRequestType.securityTokenRequestTypeIssue, - MessageSecurityMode.messageSecurityModeNone, - NULL_BYTE_STRING, - lifetime); - - - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(openSecureChannelRequest.getIdentifier())), - null, - null - ); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - openSecureChannelRequest - ); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - OpcuaOpenRequest openRequest = new OpcuaOpenRequest( - FINAL_CHUNK, - 0, - SECURITY_POLICY_NONE, - NULL_BYTE_STRING, - NULL_BYTE_STRING, - transactionId, - transactionId, - buffer.getBytes() - ); - - Consumer requestConsumer = t -> context.sendRequest(new OpcuaAPU(openRequest)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaOpenResponse) - .unwrap(p -> (OpcuaOpenResponse) p.getMessage()) - .check(p -> p.getRequestId() == transactionId) - .handle(opcuaOpenResponse -> { - try { - ExtensionObject message = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaOpenResponse.getMessage(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false); - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - } else { - LOGGER.debug("Got answer for open request"); - commonPool().submit(() -> { - try { - onDiscoverGetEndpointsRequest(context, opcuaOpenResponse, - (OpenSecureChannelResponse) message.getBody()); - } catch (PlcConnectionException e) { - LOGGER.error("Error occurred while connecting to OPC UA server"); - } - }); - } - } catch (ParseException e) { - LOGGER.debug("error caught", e); - } - }); - - channelTransactionManager.submit(requestConsumer, transactionId); - } catch (SerializationException e) { - LOGGER.error("Unable to to Parse Create Session Request"); - } } - public void onDiscoverGetEndpointsRequest(ConversationContext context, OpcuaOpenResponse opcuaOpenResponse, OpenSecureChannelResponse openSecureChannelResponse) throws PlcConnectionException { - ChannelSecurityToken securityToken = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken(); - tokenId.set((int) securityToken.getTokenId()); - channelId.set((int) securityToken.getChannelId()); - - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - int nextSequenceNumber = opcuaOpenResponse.getSequenceNumber() + 1; - int nextRequestId = opcuaOpenResponse.getRequestId() + 1; - - if (!(transactionId == nextSequenceNumber)) { - LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber); - throw new PlcConnectionException("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber); - } - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); + public CompletableFuture onDiscoverGetEndpointsRequest( + OpenSecureChannelResponse openSecureChannelResponse) { +// ChannelSecurityToken securityToken = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken(); +// securityHeader.set(new SecurityHeader(securityToken.getChannelId(), securityToken.getTokenId())); + RequestHeader requestHeader = conversation.createRequestHeader(); GetEndpointsRequest endpointsRequest = new GetEndpointsRequest( requestHeader, @@ -909,307 +421,71 @@ public void onDiscoverGetEndpointsRequest(ConversationContext context, null ); - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(endpointsRequest.getIdentifier())), - null, - null - ); + return conversation.submit(endpointsRequest, GetEndpointsResponse.class).thenApply(response -> { + List endpoints = response.getEndpoints(); + for (ExtensionObjectDefinition endpoint : endpoints) { + EndpointDescription endpointDescription = (EndpointDescription) endpoint; - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - endpointsRequest - ); + boolean urlMatch = endpointDescription.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue()); + boolean policyMatch = endpointDescription.getSecurityPolicyUri().getStringValue().equals(this.configuration.getSecurityPolicy().getSecurityPolicyUri()); + boolean msgSecurityMatch = endpointDescription.getSecurityMode().equals(this.configuration.getMessageSecurity().getMode()); - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK, - channelId.get(), - tokenId.get(), - nextSequenceNumber, - nextRequestId, - buffer.getBytes() - ); + LOGGER.debug("Validate OPC UA endpoint {} during discovery phase." + + "Expected {}. Endpoint policy {} looking for {}. Message security {}, looking for {}", endpointDescription.getEndpointUrl().getStringValue(), this.endpoint.getStringValue(), + endpointDescription.getSecurityPolicyUri().getStringValue(), configuration.getSecurityPolicy().getSecurityPolicyUri(), + endpointDescription.getSecurityMode(), configuration.getMessageSecurity().getMode()); - Consumer requestConsumer = t -> context.sendRequest(new OpcuaAPU(messageRequest)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaMessageResponse) - .unwrap(p -> (OpcuaMessageResponse) p.getMessage()) - .check(p -> p.getRequestId() == transactionId) - .handle(opcuaMessageResponse -> { - try { - ExtensionObject message = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaMessageResponse.getMessage(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN), false); - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - return; - } else { - LOGGER.debug("Got Create Session Response Connection Response"); - GetEndpointsResponse response = (GetEndpointsResponse) message.getBody(); - - List endpoints = response.getEndpoints(); - for (ExtensionObjectDefinition endpoint : endpoints) { - EndpointDescription endpointDescription = (EndpointDescription) endpoint; - - boolean urlMatch = endpointDescription.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue()); - boolean policyMatch = endpointDescription.getSecurityPolicyUri().getStringValue().equals(this.securityPolicy.getSecurityPolicyUri()); - - LOGGER.debug("Validate OPC UA endpoint {} during discovery phase." - + "Expected {}. Endpoint policy {} looking for {}", endpointDescription.getEndpointUrl().getStringValue(), this.endpoint.getStringValue(), - endpointDescription.getSecurityPolicyUri().getStringValue(), securityPolicy.getSecurityPolicyUri()); - - if (urlMatch && policyMatch) { - LOGGER.info("Found OPC UA endpoint {}", this.endpoint.getStringValue()); - configuration.setSenderCertificate(endpointDescription.getServerCertificate().getStringValue()); - break; - } - } - - if (configuration.getSenderCertificate() == null) { - throw new IllegalArgumentException(""); - } - - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); - byte[] digest = messageDigest.digest(configuration.getSenderCertificate()); - configuration.setThumbprint(new PascalByteString(digest.length, digest)); - } catch (Exception e) { - LOGGER.error("Failed to find hashing algorithm"); - } - commonPool().submit(() -> onDiscoverCloseSecureChannel(context, response)); - } - } catch (ParseException e) { - LOGGER.error("Error parsing", e); - throw new RuntimeException(e); - } - }); + if (urlMatch && policyMatch && msgSecurityMatch) { + LOGGER.info("Found OPC UA endpoint {}", this.endpoint.getStringValue()); + return endpointDescription; + } + } - channelTransactionManager.submit(requestConsumer, transactionId); - } catch (SerializationException e) { - LOGGER.error("Unable to to Parse Create Session Request"); - } + throw new IllegalArgumentException("Could not find endpoint matching client configuration"); + }); } - private void onDiscoverCloseSecureChannel(ConversationContext context, GetEndpointsResponse message) { - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - RequestHeader requestHeader = new RequestHeader( - new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, //RequestHandle - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT - ); - - CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader); - - ExpandedNodeId expandedNodeId = new ExpandedNodeId( - false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(closeSecureChannelRequest.getIdentifier())), - null, - null - ); - - OpcuaCloseRequest closeRequest = new OpcuaCloseRequest( - FINAL_CHUNK, - channelId.get(), - tokenId.get(), - transactionId, - transactionId, - new ExtensionObject( - expandedNodeId, - null, - closeSecureChannelRequest - ) - ); + private OpenSecureChannelResponse onOpenResponse(OpcuaOpenResponse opcuaOpenResponse) { + try { + ReadBuffer readBuffer = toBuffer(opcuaOpenResponse::getMessage); + ExtensionObject message = ExtensionObject.staticParse(readBuffer, false); - Consumer requestConsumer = t -> context.sendRequest(new OpcuaAPU(closeRequest)) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .check(p -> p.getMessage() instanceof OpcuaMessageResponse) - .unwrap(p -> (OpcuaMessageResponse) p.getMessage()) - .check(p -> p.getRequestId() == transactionId) - .handle(opcuaMessageResponse -> { - LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString()); - // Send an event that connection setup is complete. - context.fireDiscovered(this.configuration); - }); + if (message.getBody() instanceof ServiceFault) { + ServiceFault fault = (ServiceFault) message.getBody(); + throw new PlcRuntimeException(Conversation.toProtocolException(fault)); + } - channelTransactionManager.submit(requestConsumer, transactionId); + LOGGER.debug("Received valid answer for open secure channel request, forwarding it to call initiator"); + return (OpenSecureChannelResponse) message.getBody(); + } catch (ParseException e) { + throw new IllegalArgumentException("Could not handle response", e); + } } private void keepAlive() { - keepAlive = CompletableFuture.supplyAsync(() -> { - while (enableKeepalive.get()) { - - final Instant sendNextKeepaliveAt = Instant.now() - .plus(Duration.ofMillis((long) Math.ceil(this.lifetime * 0.75f))); - - while (Instant.now().isBefore(sendNextKeepaliveAt)) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - LOGGER.trace("Interrupted Exception"); - currentThread().interrupt(); - } - - // Do not attempt to send keepalive, if the thread has already been shut down. - if (!enableKeepalive.get()) { - return null; // exit from keepalive loop - } - } - - int transactionId = channelTransactionManager.getTransactionIdentifier(); - - RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken), - getCurrentDateTime(), - 0L, //RequestHandle - 0L, - NULL_STRING, - REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT); - - OpenSecureChannelRequest openSecureChannelRequest; - if (this.isEncrypted) { - openSecureChannelRequest = new OpenSecureChannelRequest( - requestHeader, - VERSION, - SecurityTokenRequestType.securityTokenRequestTypeIssue, - MessageSecurityMode.messageSecurityModeSignAndEncrypt, - new PascalByteString(clientNonce.length, clientNonce), - lifetime); - } else { - openSecureChannelRequest = new OpenSecureChannelRequest( - requestHeader, - VERSION, - SecurityTokenRequestType.securityTokenRequestTypeIssue, - MessageSecurityMode.messageSecurityModeNone, - NULL_BYTE_STRING, - lifetime); - } - - ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(openSecureChannelRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - openSecureChannelRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - OpcuaOpenRequest openRequest = new OpcuaOpenRequest( - FINAL_CHUNK, - 0, - new PascalString(this.securityPolicy.getSecurityPolicyUri()), - this.publicCertificate, - this.thumbprint, - transactionId, - transactionId, - buffer.getBytes() - ); - - final OpcuaAPU apu; - - if (this.isEncrypted) { - apu = OpcuaAPU.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getBytes()), false); - } else { - apu = new OpcuaAPU(openRequest); + long keepAliveTime = (long) Math.ceil(revisedLifetime * 0.75f); + LOGGER.debug("Scheduling session keep alive to happen within {}s", TimeUnit.MILLISECONDS.toSeconds(keepAliveTime)); + keepAlive = KEEP_ALIVE_EXECUTOR.schedule(() -> { + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + onConnectOpenSecureChannel(SecurityTokenRequestType.securityTokenRequestTypeRenew) + .whenComplete((response, error) -> { + if (error != null) { + transaction.failRequest(error); + return; } - - Consumer requestConsumer = t -> context.sendRequest(apu) - .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT) - .unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage)) - .check(p -> p.getMessage() instanceof OpcuaOpenResponse) - .unwrap(p -> (OpcuaOpenResponse) p.getMessage()) - .check(p -> { - if (p.getRequestId() == transactionId) { - senderSequenceNumber.incrementAndGet(); - return true; - } else { - return false; - } - }) - .handle(opcuaOpenResponse -> { - try { - ReadBufferByteBased readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); - ExtensionObject message = ExtensionObject.staticParse(readBuffer, false); - - if (message.getBody() instanceof ServiceFault) { - ServiceFault fault = (ServiceFault) message.getBody(); - LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode())); - } else { - LOGGER.debug("Got keep alive response"); - OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody(); - ChannelSecurityToken token = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken(); - tokenId.set((int) token.getTokenId()); - channelId.set((int) token.getChannelId()); - lifetime = token.getRevisedLifetime(); - } - } catch (ParseException e) { - LOGGER.error("parse exception caught", e); - } - }); - channelTransactionManager.submit(requestConsumer, transactionId); - } catch (SerializationException | ParseException e) { - LOGGER.error("Unable to to Parse Open Secure Request"); - } - } - return null; - }, - newSingleThreadExecutor() - ); + transaction.endRequest(); + }); + }); + }, keepAliveTime, TimeUnit.MILLISECONDS); } - /** - * Returns the next request handle - * - * @return the next sequential request handle - */ - public int getRequestHandle() { - int transactionId = requestHandleGenerator.getAndIncrement(); - if (requestHandleGenerator.get() == SecureChannelTransactionManager.DEFAULT_MAX_REQUEST_ID) { - requestHandleGenerator.set(1); + private static ReadBufferByteBased toBuffer(Supplier supplier) { + Payload payload = supplier.get(); + if (!(payload instanceof BinaryPayload)) { + throw new IllegalArgumentException("Unexpected payload kind"); } - return transactionId; - } - - /** - * Returns the authentication token for the current connection - * - * @return a NodeId Authentication token - */ - public NodeId getAuthenticationToken() { - return new NodeId(this.authenticationToken); - } - - /** - * Gets the Channel identifier for the current channel - * - * @return int representing the channel identifier - */ - public int getChannelId() { - return this.channelId.get(); - } - - /** - * Gets the Token Identifier - * - * @return int representing the token identifier - */ - public int getTokenId() { - return this.tokenId.get(); + return new ReadBufferByteBased(((BinaryPayload) payload).getPayload(), org.apache.plc4x.java.spi.generation.ByteOrder.LITTLE_ENDIAN); } /** @@ -1325,17 +601,18 @@ private ExtensionObject getIdentityToken(UserTokenType tokenType, String securit new UserIdentityToken(new PascalString(securityPolicy), anonymousIdentityToken)); case userTokenTypeUserName: //Encrypt the password using the server nonce and server public key + byte[] remoteNonce = conversation.getRemoteNonce(); byte[] passwordBytes = this.password == null ? new byte[0] : this.password.getBytes(); - ByteBuffer encodeableBuffer = ByteBuffer.allocate(4 + passwordBytes.length + this.senderNonce.length); + ByteBuffer encodeableBuffer = ByteBuffer.allocate(4 + passwordBytes.length + remoteNonce.length); encodeableBuffer.order(ByteOrder.LITTLE_ENDIAN); - encodeableBuffer.putInt(passwordBytes.length + this.senderNonce.length); + encodeableBuffer.putInt(passwordBytes.length + remoteNonce.length); encodeableBuffer.put(passwordBytes); - encodeableBuffer.put(this.senderNonce); - byte[] encodeablePassword = new byte[4 + passwordBytes.length + this.senderNonce.length]; + encodeableBuffer.put(remoteNonce); + byte[] encodeablePassword = new byte[4 + passwordBytes.length + remoteNonce.length]; encodeableBuffer.position(0); encodeableBuffer.get(encodeablePassword); - byte[] encryptedPassword = encryptionHandler.encryptPassword(encodeablePassword); + byte[] encryptedPassword = conversation.encryptPassword(encodeablePassword); UserNameIdentityToken userNameIdentityToken = new UserNameIdentityToken( new PascalString(this.username), new PascalByteString(encryptedPassword.length, encryptedPassword), @@ -1356,8 +633,21 @@ private ExtensionObject getIdentityToken(UserTokenType tokenType, String securit return null; } - public static long getCurrentDateTime() { - return (System.currentTimeMillis() * 10000) + EPOCH_OFFSET; + public static X509Certificate getX509Certificate(byte[] certificate) { + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certificate)); + } catch (Exception e) { + LOGGER.error("Unable to get certificate from String {}", certificate); + return null; + } + } + + private static PascalByteString createPascalString(byte[] bytes) { + if (null == bytes) { + return NULL_BYTE_STRING; + } + return new PascalByteString(bytes.length, bytes); } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java index bea8a1a907d..0690519f22f 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java @@ -18,44 +18,18 @@ */ package org.apache.plc4x.java.opcua.context; -import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - public class SecureChannelTransactionManager { - private static final Logger LOGGER = LoggerFactory.getLogger(SecureChannel.class); public static final int DEFAULT_MAX_REQUEST_ID = 0xFFFFFFFF; - private final AtomicInteger transactionIdentifierGenerator = new AtomicInteger(0); - private final AtomicInteger requestIdentifierGenerator = new AtomicInteger(0); - private final AtomicInteger activeTransactionId = new AtomicInteger(0); - private final Map queue = new HashMap<>(); - public synchronized void submit(Consumer onSend, Integer transactionId) { - LOGGER.info("Active transaction Number {}", activeTransactionId.get()); - if (activeTransactionId.get() == transactionId) { - onSend.accept(transactionId); - int newTransactionId = getActiveTransactionIdentifier(); - if (!queue.isEmpty()) { - Transaction t = queue.remove(newTransactionId); - if (t == null) { - LOGGER.info("Length of Queue is {}", queue.size()); - LOGGER.info("Transaction ID is {}", newTransactionId); - LOGGER.info("Map is {}", queue); - throw new PlcRuntimeException("Transaction Id not found in queued messages {}"); - } - submit(t.getConsumer(), t.getTransactionId()); - } - } else { - LOGGER.info("Storing out of order transaction {}", transactionId); - queue.put(transactionId, new Transaction(onSend, transactionId)); - } - } + private final AtomicInteger transactionIdentifierGenerator = new AtomicInteger(1); + private final AtomicInteger sequenceIdGenerator = new AtomicInteger(1); + private final AtomicInteger requestHandleGenerator = new AtomicInteger(1); /** * Returns the next transaction identifier. @@ -63,43 +37,47 @@ public synchronized void submit(Consumer onSend, Integer transactionId) * @return the next sequential transaction identifier */ public int getTransactionIdentifier() { + // transaction identifier must begin with 1, otherwise .NET standard server fails! int transactionId = transactionIdentifierGenerator.getAndIncrement(); - if(transactionIdentifierGenerator.get() == DEFAULT_MAX_REQUEST_ID) { - transactionIdentifierGenerator.set(1); + if (transactionId == DEFAULT_MAX_REQUEST_ID) { + transactionIdentifierGenerator.set(0); } return transactionId; } /** - * Returns the next transaction identifier. + * Returns the next sequence identifier. * - * @return the next sequential transaction identifier + * @return the next sequential identifier */ - private int getActiveTransactionIdentifier() { - int transactionId = activeTransactionId.incrementAndGet(); - if(activeTransactionId.get() == DEFAULT_MAX_REQUEST_ID) { - activeTransactionId.set(1); + private int getSequenceIdentifier() { + int sequenceId = sequenceIdGenerator.getAndIncrement(); + if (sequenceId == DEFAULT_MAX_REQUEST_ID) { + sequenceIdGenerator.set(0); } - return transactionId; + return sequenceId; } - public static class Transaction { - - private final Integer transactionId; - private final Consumer consumer; - - public Transaction(Consumer consumer, Integer transactionId) { - this.consumer = consumer; - this.transactionId = transactionId; - } - - public Integer getTransactionId() { - return transactionId; - } + /** + * Creates sequence supplier for temporary use by message sender. + * + * @return Sequence supplier. + */ + public Supplier getSequenceSupplier() { + return this::getSequenceIdentifier; + } - public Consumer getConsumer() { - return consumer; + /** + * Returns the next request handle + * + * @return the next sequential request handle + */ + public int getRequestHandle() { + int transactionId = requestHandleGenerator.getAndIncrement(); + if (requestHandleGenerator.get() == SecureChannelTransactionManager.DEFAULT_MAX_REQUEST_ID) { + requestHandleGenerator.set(0); } + return transactionId; } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SymmetricEncryptionHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SymmetricEncryptionHandler.java index b7d80316d03..657b90fd818 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SymmetricEncryptionHandler.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SymmetricEncryptionHandler.java @@ -1,9 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ package org.apache.plc4x.java.opcua.context; -import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; -import org.apache.plc4x.java.opcua.readwrite.MessagePDU; -import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU; -import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageResponse; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import org.apache.plc4x.java.opcua.protocol.chunk.Chunk; import org.apache.plc4x.java.opcua.security.SecurityPolicy; import org.apache.plc4x.java.opcua.security.SecurityPolicy.EncryptionAlgorithm; import org.apache.plc4x.java.opcua.security.SecurityPolicy.MacSignatureAlgorithm; @@ -15,158 +32,85 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -public class SymmetricEncryptionHandler { - private static final int SECURE_MESSAGE_HEADER_SIZE = 12; - private static final int SEQUENCE_HEADER_SIZE = 8; - private static final int SYMMETRIC_SECURITY_HEADER_SIZE = 4; - - private final SecurityPolicy policy; - +public class SymmetricEncryptionHandler extends BaseEncryptionHandler { private SymmetricKeys keys = null; - public SymmetricEncryptionHandler(SecurityPolicy policy) { - this.policy = policy; + public SymmetricEncryptionHandler(Conversation channel, SecurityPolicy policy) { + super(channel, policy); } - /** - * Docs: https://reference.opcfoundation.org/Core/Part6/v104/docs/6.7 - * - * @param pdu - * @param message - * @param clientNonce - * @param serverNonce - * @return - */ - public ReadBuffer encodeMessage(MessagePDU pdu, byte[] message, byte[] clientNonce, byte[] serverNonce) { - int unencryptedLength = pdu.getLengthInBytes(); - int messageLength = message.length; - - int beforeBodyLength = unencryptedLength - messageLength; // message header, security header, sequence header - - int cipherTextBlockSize = 16; - int plainTextBlockSize = 16; - int signatureSize = policy.getSymmetricSignatureAlgorithm().getSymmetricSignatureSize(); - - - int maxChunkSize = 8196; - int paddingOverhead = 1; - - - int securityHeaderSize = SYMMETRIC_SECURITY_HEADER_SIZE; - int maxCipherTextSize = maxChunkSize - securityHeaderSize; - int maxCipherTextBlocks = maxCipherTextSize / cipherTextBlockSize; - int maxPlainTextSize = maxCipherTextBlocks * plainTextBlockSize; - int maxBodySize = maxPlainTextSize - SEQUENCE_HEADER_SIZE - paddingOverhead - signatureSize; - - int bodySize = Math.min(message.length, maxBodySize); - - int plainTextSize = SEQUENCE_HEADER_SIZE + bodySize + paddingOverhead + signatureSize; - int remaining = plainTextSize % plainTextBlockSize; - int paddingSize = remaining > 0 ? plainTextBlockSize - remaining : 0; - - int plainTextContentSize = SEQUENCE_HEADER_SIZE + bodySize + - signatureSize + paddingSize + paddingOverhead; - - int frameSize = SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize + - (plainTextContentSize / plainTextBlockSize) * cipherTextBlockSize; - - SymmetricKeys symmetricKeys = getSymmetricKeys(clientNonce, serverNonce); + protected void verify(WriteBufferByteBased buffer, Chunk chunk, int messageLength) throws Exception { + int signatureStart = messageLength - chunk.getSignatureSize(); + byte[] message = buffer.getBytes(0, signatureStart); + byte[] signatureData = buffer.getBytes(signatureStart, signatureStart + chunk.getSignatureSize()); - try { - WriteBufferByteBased buf = new WriteBufferByteBased(frameSize, ByteOrder.LITTLE_ENDIAN); - OpcuaAPU opcuaAPU = new OpcuaAPU(pdu); - opcuaAPU.serialize(buf); + SymmetricKeys symmetricKeys = getSymmetricKeys(conversation.getLocalNonce(), conversation.getRemoteNonce()); + MacSignatureAlgorithm algorithm = securityPolicy.getSymmetricSignatureAlgorithm(); + Mac signature = algorithm.getSignature(); + signature.init(new SecretKeySpec(symmetricKeys.getServerKeys().getSignatureKey(), algorithm.getName())); + signature.update(message); + byte[] signatureBytes = signature.doFinal(); - writePadding(paddingSize, buf); - updateFrameSize(frameSize, buf); - - byte[] sign = sign(buf.getBytes(), symmetricKeys.getClientKeys()); - buf.writeByteArray(sign); - - buf.setPos(SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize); - - byte[] encrypted = encrypt(securityHeaderSize, frameSize, buf, symmetricKeys); - buf.writeByteArray(encrypted); - - return new ReadBufferByteBased(buf.getBytes(), ByteOrder.LITTLE_ENDIAN); - } catch (Exception e) { - throw new RuntimeException(e); + if (!MessageDigest.isEqual(signatureData, signatureBytes)) { + throw new IllegalArgumentException("Invalid signature"); } } - public OpcuaAPU decodeMessage(OpcuaAPU pdu, byte[] clientNonce, byte[] serverNonce) { - MessagePDU message = pdu.getMessage(); - - OpcuaMessageResponse a = (OpcuaMessageResponse) message; - - - int cipherTextBlockSize = 16; // different for aes256 - - if (!(a.getMessage() instanceof BinaryPayload)) { - throw new IllegalArgumentException("Unexpected payload"); - } - byte[] textMessage = ((BinaryPayload) a.getMessage()).getPayload(); - - - int blockCount = (SEQUENCE_HEADER_SIZE + textMessage.length) / cipherTextBlockSize; - int plainTextBufferSize = cipherTextBlockSize * blockCount; - - - try { - WriteBufferByteBased buf = new WriteBufferByteBased(pdu.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - pdu.serialize(buf); + protected int decrypt(WriteBufferByteBased chunkBuffer, Chunk chunk, int messageLength) throws Exception { + int bodyStart = 12 + chunk.getSecurityHeaderSize(); - EncryptionAlgorithm transformation = policy.getSymmetricEncryptionAlgorithm(); - SymmetricKeys symmetricKeys = getSymmetricKeys(clientNonce, serverNonce); - Cipher cipher = getCipher(symmetricKeys.getServerKeys(), transformation, Cipher.DECRYPT_MODE); + int bodySize = messageLength - bodyStart; + int blockCount = bodySize / chunk.getCipherTextBlockSize(); + assert(bodySize % chunk.getCipherTextBlockSize() == 0); - ByteBuffer buffer = ByteBuffer.allocate(plainTextBufferSize); - byte[] bytes = buf.getBytes(pdu.getLengthInBytes() - plainTextBufferSize, pdu.getLengthInBytes()); - ByteBuffer originalMessage = ByteBuffer.wrap(bytes); + byte[] encrypted = chunkBuffer.getBytes(bodyStart, bodyStart + bodySize); + byte[] plainText = new byte[chunk.getCipherTextBlockSize() * blockCount]; + SymmetricKeys symmetricKeys = getSymmetricKeys(conversation.getLocalNonce(), conversation.getRemoteNonce()); + Cipher cipher = getCipher(symmetricKeys.getServerKeys(), securityPolicy.getSymmetricEncryptionAlgorithm(), Cipher.DECRYPT_MODE); - cipher.doFinal(originalMessage, buffer); - - buffer.flip(); - - buf.setPos(pdu.getLengthInBytes() - plainTextBufferSize); - buf.writeByteArray(buffer.array()); - - - int frameSize = pdu.getLengthInBytes() - plainTextBufferSize + buffer.limit(); - - updateFrameSize(frameSize, buf); - - byte[] decryptedMessage = buf.getBytes(0, frameSize); - ReadBuffer readBuffer = new ReadBufferByteBased(decryptedMessage, ByteOrder.LITTLE_ENDIAN); - OpcuaAPU opcuaAPU = OpcuaAPU.staticParse(readBuffer, true); - return opcuaAPU; - } catch (Exception e) { - throw new RuntimeException(e); - } - + int bodyLength = cipher.doFinal(encrypted, 0, encrypted.length, plainText, 0); + chunkBuffer.setPos(bodyStart); + chunkBuffer.writeByteArray("payload", plainText); + return bodyLength; } - private byte[] encrypt(int securityHeaderSize, int frameSize, WriteBufferByteBased buf, SymmetricKeys symmetricKeys) throws Exception { - ByteBuffer buffer = ByteBuffer.allocate(frameSize - buf.getPos()); - ByteBuffer originalMessage = ByteBuffer.wrap(buf.getBytes(SECURE_MESSAGE_HEADER_SIZE + securityHeaderSize, frameSize)); + protected void encrypt(WriteBufferByteBased buffer, int securityHeaderSize, int plainTextBlockSize, int cipherTextBlockSize, int blockCount) throws Exception { + SymmetricKeys symmetricKeys = getSymmetricKeys(conversation.getLocalNonce(), conversation.getRemoteNonce()); + int bodyStart = 12 + securityHeaderSize; + byte[] copy = buffer.getBytes(bodyStart, bodyStart + (plainTextBlockSize * blockCount)); + byte[] encrypted = new byte[cipherTextBlockSize * blockCount]; - EncryptionAlgorithm transformation = policy.getSymmetricEncryptionAlgorithm(); + EncryptionAlgorithm transformation = securityPolicy.getSymmetricEncryptionAlgorithm(); Cipher cipher = getCipher(symmetricKeys.getClientKeys(), transformation, Cipher.ENCRYPT_MODE); + cipher.doFinal(copy, 0, copy.length, encrypted, 0); + buffer.setPos(bodyStart); + buffer.writeByteArray("encrypted", encrypted); + } - cipher.doFinal(originalMessage, buffer); + protected byte[] sign(byte[] data)throws GeneralSecurityException { + SymmetricKeys symmetricKeys = getSymmetricKeys(conversation.getLocalNonce(), conversation.getRemoteNonce()); + MacSignatureAlgorithm algorithm = securityPolicy.getSymmetricSignatureAlgorithm(); + Mac signature = algorithm.getSignature(); + signature.init(new SecretKeySpec(symmetricKeys.getClientKeys().getSignatureKey(), algorithm.getName())); + signature.update(data); + return signature.doFinal(); + } - return buffer.array(); + private SymmetricKeys getSymmetricKeys(byte[] senderNonce, byte[] receiverNonce) { + if (keys == null) { + keys = SymmetricKeys.generateKeyPair(senderNonce, receiverNonce, securityPolicy); + } + return keys; } private static Cipher getCipher(SymmetricKeys.Keys symmetricKeys, EncryptionAlgorithm transformation, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { @@ -179,37 +123,4 @@ private static Cipher getCipher(SymmetricKeys.Keys symmetricKeys, EncryptionAlgo return cipher; } - private static void updateFrameSize(int frameSize, WriteBufferByteBased buf) throws SerializationException { - int initPosition = buf.getPos(); - buf.setPos(4); - buf.writeInt(32, frameSize); - buf.setPos(initPosition); - } - - - public byte[] sign(byte[] data, SymmetricKeys.Keys symmetricKeys) { - try { - MacSignatureAlgorithm algorithm = policy.getSymmetricSignatureAlgorithm(); - Mac signature = algorithm.getSignature(); - signature.init(new SecretKeySpec(symmetricKeys.getSignatureKey(), algorithm.getName())); - signature.update(data); - return signature.doFinal(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private void writePadding(int paddingSize, WriteBufferByteBased buffer) throws Exception { - buffer.writeByte((byte) paddingSize); - for (int i = 0; i < paddingSize; i++) { - buffer.writeByte((byte) paddingSize); - } - } - - private SymmetricKeys getSymmetricKeys(byte[] clientNonce, byte[] serverNonce) { - if (keys == null) { - keys = SymmetricKeys.generateKeyPair(clientNonce, serverNonce, policy.getSymmetricSignatureAlgorithm()); - } - return keys; - } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java index 071914a3514..88d6e06c674 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java @@ -18,9 +18,10 @@ */ package org.apache.plc4x.java.opcua.protocol; -import static java.util.concurrent.ForkJoinPool.commonPool; +import static org.apache.plc4x.java.opcua.context.SecureChannel.getX509Certificate; import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; import org.apache.plc4x.java.api.authentication.PlcAuthentication; import org.apache.plc4x.java.api.exceptions.PlcConnectionException; import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; @@ -31,6 +32,7 @@ import org.apache.plc4x.java.api.types.PlcValueType; import org.apache.plc4x.java.api.value.PlcValue; import org.apache.plc4x.java.opcua.config.OpcuaConfiguration; +import org.apache.plc4x.java.opcua.context.Conversation; import org.apache.plc4x.java.opcua.context.OpcuaDriverContext; import org.apache.plc4x.java.opcua.context.SecureChannel; import org.apache.plc4x.java.opcua.readwrite.*; @@ -39,11 +41,12 @@ import org.apache.plc4x.java.spi.Plc4xProtocolBase; import org.apache.plc4x.java.spi.configuration.HasConfiguration; import org.apache.plc4x.java.spi.context.DriverContext; -import org.apache.plc4x.java.spi.generation.*; import org.apache.plc4x.java.spi.messages.*; import org.apache.plc4x.java.spi.messages.utils.ResponseItem; import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration; import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionTag; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager.RequestTransaction; import org.apache.plc4x.java.spi.values.PlcList; import org.apache.plc4x.java.spi.values.PlcValueHandler; import org.slf4j.Logger; @@ -56,9 +59,6 @@ import java.time.ZoneOffset; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.BiConsumer; import java.util.function.Consumer; public class OpcuaProtocolLogic extends Plc4xProtocolBase implements HasConfiguration, PlcSubscriber { @@ -78,9 +78,13 @@ public class OpcuaProtocolLogic extends Plc4xProtocolBase implements H new NullExtension()); // Body private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time. + private final Map subscriptions = new ConcurrentHashMap<>(); + private final RequestTransactionManager tm = new RequestTransactionManager(); + private OpcuaConfiguration configuration; - private final Map subscriptions = new HashMap<>(); + private OpcuaDriverContext driverContext; private SecureChannel channel; + private Conversation conversation; @Override public void setConfiguration(OpcuaConfiguration configuration) { @@ -100,12 +104,13 @@ public void onDisconnect(ConversationContext context) { for (Map.Entry subscriber : subscriptions.entrySet()) { subscriber.getValue().stopSubscriber(); } - commonPool().submit(() -> channel.onDisconnect(context)); + + channel.onDisconnect(); } @Override public void setDriverContext(DriverContext driverContext) { - super.setDriverContext(driverContext); + this.driverContext = (OpcuaDriverContext) driverContext; } @Override @@ -114,48 +119,74 @@ public void onConnect(ConversationContext context) { if (this.channel == null) { try { - this.channel = createSecureChannel(context.getAuthentication()); + this.channel = createSecureChannel(context, context.getAuthentication()); } catch (PlcRuntimeException ex) { context.getChannel().pipeline().fireExceptionCaught(new PlcConnectionException(ex)); return; } } - commonPool().submit(() -> this.channel.onConnect(context)); + + CompletableFuture future = new CompletableFuture<>(); + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + channel.onConnect().whenComplete(((response, error) -> bridge(transaction, future, response, error))); + }); + future.whenComplete((response, error) -> { + if (error != null) { + LOGGER.error("Failed to establish connection", error); + return; + } + LOGGER.error("Established connection to server", error); + context.fireConnected(); + }); } @Override public void onDiscover(ConversationContext context) { + if (!driverContext.getEncrypted()) { + LOGGER.debug("not encrypted, ignoring onDiscover"); + context.fireDiscovered(configuration); + return; + } + // Only the TCP transport supports login. LOGGER.debug("Opcua Driver running in ACTIVE mode, discovering endpoints"); if (this.channel == null) { try { - this.channel = createSecureChannel(context.getAuthentication()); + this.channel = createSecureChannel(context, context.getAuthentication()); } catch (PlcRuntimeException ex) { context.getChannel().pipeline().fireExceptionCaught(new PlcConnectionException(ex)); return; } } - commonPool().submit(() -> channel.onDiscover(context)); + + CompletableFuture future = new CompletableFuture<>(); + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> + channel.onDiscover().whenComplete((response, error) -> bridge(transaction, future, response, error)) + ); + future.whenComplete((response, error) -> { + if (error != null) { + context.getChannel().pipeline().fireExceptionCaught(new PlcConnectionException(error)); + return; + } + configuration.setServerCertificate(getX509Certificate(response.getServerCertificate().getStringValue())); + context.fireDiscovered(configuration); + context.fireDisconnected(); + }); } - private SecureChannel createSecureChannel(PlcAuthentication authentication) { - return new SecureChannel((OpcuaDriverContext) driverContext, configuration, authentication); + private SecureChannel createSecureChannel(ConversationContext context, PlcAuthentication authentication) { + this.conversation = new Conversation(context, driverContext, configuration); + return new SecureChannel(conversation, tm, driverContext, configuration, authentication); } @Override public CompletableFuture read(PlcReadRequest readRequest) { LOGGER.trace("Reading Value"); - CompletableFuture future = new CompletableFuture<>(); DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest; - - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - channel.getRequestHandle(), - 0L, - NULL_STRING, - SecureChannel.REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT); + RequestHeader requestHeader = conversation.createRequestHeader(); List readValueArray = new ArrayList<>(request.getTagNames().size()); Iterator iterator = request.getTagNames().iterator(); @@ -178,64 +209,12 @@ public CompletableFuture read(PlcReadRequest readRequest) { readValueArray.size(), readValueArray); - ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(opcuaReadRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - opcuaReadRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - /* Functional Consumer example using inner class */ - Consumer consumer = opcuaResponse -> { - try { - ExtensionObjectDefinition reply = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (reply instanceof ReadResponse) { - future.complete(new DefaultPlcReadResponse(request, readResponse(request.getTagNames(), ((ReadResponse) reply).getResults()))); - } else { - if (reply instanceof ServiceFault) { - ExtensionObjectDefinition header = ((ServiceFault) reply).getResponseHeader(); - LOGGER.error("Read request ended up with ServiceFault: {}", header); - } else { - LOGGER.error("Remote party returned an error '{}'", reply); - } - - Map> status = new LinkedHashMap<>(); - for (String key : request.getTagNames()) { - status.put(key, new ResponseItem<>(PlcResponseCode.INTERNAL_ERROR, null)); - } - future.complete(new DefaultPlcReadResponse(request, status)); - } - } catch (ParseException | PlcRuntimeException e) { - future.completeExceptionally(new PlcRuntimeException(e)); - } - }; - - /* Functional Consumer example using inner class */ - // Pass the response back to the application. - Consumer timeout = future::completeExceptionally; - - /* Functional Consumer example using inner class */ - BiConsumer error = (message, t) -> { - - // Pass the response back to the application. - future.completeExceptionally(t); - }; - - channel.submit(context, timeout, error, consumer, buffer); - - } catch (SerializationException e) { - LOGGER.error("Unable to serialise the ReadRequest"); - } - - return future; + CompletableFuture future = new CompletableFuture<>(); + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + conversation.submit(opcuaReadRequest, ReadResponse.class).whenComplete((response, error) -> bridge(transaction, future, response, error)); + }); + return future.thenApply(response -> new DefaultPlcReadResponse(request, readResponse(request.getTagNames(), response.getResults()))); } static NodeId generateNodeId(OpcuaTag tag) { @@ -261,7 +240,7 @@ static NodeId generateNodeId(OpcuaTag tag) { } public Map> readResponse(LinkedHashSet tagNames, List results) { - PlcResponseCode responseCode = PlcResponseCode.OK; + PlcResponseCode responseCode = null; // initialize variable Map> response = new HashMap<>(); int count = 0; for (String tagName : tagNames) { @@ -415,12 +394,13 @@ public Map> readResponse(LinkedHashSet ta responseCode = PlcResponseCode.UNSUPPORTED; LOGGER.error("Data type - " + variant.getClass() + " is not supported "); } - } else { - if (results.get(count).getStatusCode().getStatusCode() == OpcuaStatusCode.BadNodeIdUnknown.getValue()) { - responseCode = PlcResponseCode.NOT_FOUND; - } else { - responseCode = PlcResponseCode.UNSUPPORTED; + // response code might be null in first iteration + if (PlcResponseCode.UNSUPPORTED != responseCode) { + responseCode = PlcResponseCode.OK; } + } else { + StatusCode statusCode = results.get(count).getStatusCode(); + responseCode = mapOpcStatusCode(statusCode.getStatusCode(), PlcResponseCode.UNSUPPORTED); LOGGER.error("Error while reading value from OPC UA server error code:- " + results.get(count).getStatusCode().toString()); } count++; @@ -429,12 +409,36 @@ public Map> readResponse(LinkedHashSet ta return response; } + private static PlcResponseCode mapOpcStatusCode(long opcStatusCode, PlcResponseCode fallback) { + if (!OpcuaStatusCode.isDefined(opcStatusCode)) { + return PlcResponseCode.INTERNAL_ERROR; + } + + OpcuaStatusCode statusCode = OpcuaStatusCode.enumForValue(opcStatusCode); + if (statusCode == OpcuaStatusCode.Good) { + return PlcResponseCode.OK; + } else if (statusCode == OpcuaStatusCode.BadNodeIdUnknown) { + return PlcResponseCode.NOT_FOUND; + } else if (statusCode == OpcuaStatusCode.BadTypeMismatch) { + return PlcResponseCode.INVALID_DATATYPE; + } else if (statusCode == OpcuaStatusCode.BadNotWritable) { + return PlcResponseCode.ACCESS_DENIED; + } else if (statusCode == OpcuaStatusCode.BadUserAccessDenied) { + return PlcResponseCode.ACCESS_DENIED; + } else if (statusCode == OpcuaStatusCode.BadAttributeIdInvalid) { + return PlcResponseCode.INVALID_ADDRESS; + } else if (statusCode == OpcuaStatusCode.BadIndexRangeNoData) { + return PlcResponseCode.INVALID_ADDRESS; + } + return fallback; + } + private Variant fromPlcValue(String tagName, OpcuaTag tag, PlcWriteRequest request) { PlcList valueObject; if (request.getPlcValue(tagName).getObject() instanceof ArrayList) { valueObject = (PlcList) request.getPlcValue(tagName); } else { - ArrayList list = new ArrayList<>(); + List list = new ArrayList<>(); list.add(request.getPlcValue(tagName)); valueObject = new PlcList(list); } @@ -695,17 +699,9 @@ private Variant fromPlcValue(String tagName, OpcuaTag tag, PlcWriteRequest reque @Override public CompletableFuture write(PlcWriteRequest writeRequest) { LOGGER.trace("Writing Value"); - CompletableFuture future = new CompletableFuture<>(); DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest; - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - channel.getRequestHandle(), - 0L, - NULL_STRING, - SecureChannel.REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT); - + RequestHeader requestHeader = conversation.createRequestHeader(); List writeValueList = new ArrayList<>(request.getTagNames().size()); for (String tagName : request.getTagNames()) { OpcuaTag tag = (OpcuaTag) request.getTag(tagName); @@ -730,72 +726,14 @@ public CompletableFuture write(PlcWriteRequest writeRequest) { null))); } - WriteRequest opcuaWriteRequest = new WriteRequest( - requestHeader, - writeValueList.size(), - writeValueList); - - ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(opcuaWriteRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - opcuaWriteRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - /* Functional Consumer example using inner class */ - Consumer consumer = opcuaResponse -> { - try { - ExtensionObjectDefinition reply = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (reply instanceof WriteResponse) { - WriteResponse responseMessage = (WriteResponse) reply; - PlcWriteResponse response = writeResponse(request, responseMessage); - - // Pass the response back to the application. - future.complete(response); - } else { - if (reply instanceof ServiceFault) { - ExtensionObjectDefinition header = ((ServiceFault) reply).getResponseHeader(); - LOGGER.error("Write request ended up with ServiceFault: {}", header); - } else { - LOGGER.error("Remote party returned an error '{}'", reply); - } - - Map status = new LinkedHashMap<>(); - for (String key : request.getTagNames()) { - status.put(key, PlcResponseCode.INTERNAL_ERROR); - } - future.complete(new DefaultPlcWriteResponse(request, status)); - } - } catch (ParseException e) { - throw new PlcRuntimeException(e); - } - }; - - /* Functional Consumer example using inner class */ - // Pass the response back to the application. - Consumer timeout = future::completeExceptionally; - - /* Functional Consumer example using inner class */ - BiConsumer error = (message, t) -> { - // Pass the response back to the application. - future.completeExceptionally(t); - }; - - channel.submit(context, timeout, error, consumer, buffer); + WriteRequest opcuaWriteRequest = new WriteRequest(requestHeader, writeValueList.size(), writeValueList); - } catch (SerializationException e) { - LOGGER.error("Unable to serialise the ReadRequest"); - } - - return future; + CompletableFuture future = new CompletableFuture<>(); + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + conversation.submit(opcuaWriteRequest, WriteResponse.class).whenComplete((response, error) -> bridge(transaction, future, response, error)); + }); + return future.thenApply(response -> writeResponse(request, response)); } private PlcWriteResponse writeResponse(DefaultPlcWriteRequest request, WriteResponse writeResponse) { @@ -804,17 +742,9 @@ private PlcWriteResponse writeResponse(DefaultPlcWriteRequest request, WriteResp Iterator responseIterator = request.getTagNames().iterator(); for (int i = 0; i < request.getTagNames().size(); i++) { String tagName = responseIterator.next(); - OpcuaStatusCode statusCode = OpcuaStatusCode.enumForValue(results.get(i).getStatusCode()); - switch (statusCode) { - case Good: - responseMap.put(tagName, PlcResponseCode.OK); - break; - case BadNodeIdUnknown: - responseMap.put(tagName, PlcResponseCode.NOT_FOUND); - break; - default: - responseMap.put(tagName, PlcResponseCode.REMOTE_ERROR); - } + long opcStatusCode = results.get(i).getStatusCode(); + PlcResponseCode statusCode = mapOpcStatusCode(opcStatusCode, PlcResponseCode.REMOTE_ERROR); + responseMap.put(tagName, statusCode); } return new DefaultPlcWriteResponse(request, responseMap); } @@ -822,45 +752,47 @@ private PlcWriteResponse writeResponse(DefaultPlcWriteRequest request, WriteResp @Override public CompletableFuture subscribe(PlcSubscriptionRequest subscriptionRequest) { - return CompletableFuture.supplyAsync(() -> { - Map> values = new HashMap<>(); - long subscriptionId; - ArrayList tagNames = new ArrayList<>(subscriptionRequest.getTagNames()); - long cycleTime = (subscriptionRequest.getTag(tagNames.get(0))).getDuration().orElse(Duration.ofMillis(1000)).toMillis(); - - try { - CompletableFuture subscription = onSubscribeCreateSubscription(cycleTime); - CreateSubscriptionResponse response = subscription.get(SecureChannel.REQUEST_TIMEOUT_LONG, TimeUnit.MILLISECONDS); - subscriptionId = response.getSubscriptionId(); - subscriptions.put(subscriptionId, new OpcuaSubscriptionHandle(context, this, channel, subscriptionRequest, subscriptionId, cycleTime)); - } catch (Exception e) { - throw new PlcRuntimeException("Unable to subscribe because of: " + e.getMessage()); - } - - for (String tagName : subscriptionRequest.getTagNames()) { - final DefaultPlcSubscriptionTag tagDefaultPlcSubscription = (DefaultPlcSubscriptionTag) subscriptionRequest.getTag(tagName); - if (!(tagDefaultPlcSubscription.getTag() instanceof OpcuaTag)) { - values.put(tagName, new ResponseItem<>(PlcResponseCode.INVALID_ADDRESS, null)); - } else { - values.put(tagName, new ResponseItem<>(PlcResponseCode.OK, subscriptions.get(subscriptionId))); + List tagNames = new ArrayList<>(subscriptionRequest.getTagNames()); + long cycleTime = (subscriptionRequest.getTag(tagNames.get(0))).getDuration().orElse(Duration.ofMillis(1000)).toMillis(); + + CompletableFuture future = new CompletableFuture<>(); + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + // bridge(transaction, future, response, error) + onSubscribeCreateSubscription(cycleTime).thenApply(response -> { + long subscriptionId = response.getSubscriptionId(); + OpcuaSubscriptionHandle handle = new OpcuaSubscriptionHandle(this, tm, + conversation, subscriptionRequest, subscriptionId, cycleTime); + subscriptions.put(handle.getSubscriptionId(), handle); + return handle; + }) + .thenCompose(handle -> handle.onSubscribeCreateMonitoredItemsRequest()) + .thenApply(handle -> { + Map> values = new HashMap<>(); + for (String tagName : subscriptionRequest.getTagNames()) { + final DefaultPlcSubscriptionTag tagDefaultPlcSubscription = (DefaultPlcSubscriptionTag) subscriptionRequest.getTag(tagName); + if (!(tagDefaultPlcSubscription.getTag() instanceof OpcuaTag)) { + values.put(tagName, new ResponseItem<>(PlcResponseCode.INVALID_ADDRESS, null)); + } else { + values.put(tagName, new ResponseItem<>(PlcResponseCode.OK, handle)); + } } - } - return new DefaultPlcSubscriptionResponse(subscriptionRequest, values); + + return new DefaultPlcSubscriptionResponse(subscriptionRequest, values); + }) + .whenComplete((response, error) -> bridge(transaction, future, response, error)); }); + return future; + } + + protected void requestSubscriptionPublish() { + } private CompletableFuture onSubscribeCreateSubscription(long cycleTime) { - CompletableFuture future = new CompletableFuture<>(); LOGGER.trace("Entering creating subscription request"); - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - channel.getRequestHandle(), - 0L, - NULL_STRING, - SecureChannel.REQUEST_TIMEOUT_LONG, - NULL_EXTENSION_OBJECT); - + RequestHeader requestHeader = conversation.createRequestHeader(); CreateSubscriptionRequest createSubscriptionRequest = new CreateSubscriptionRequest( requestHeader, cycleTime, @@ -871,69 +803,7 @@ private CompletableFuture onSubscribeCreateSubscript (short) 0 ); - ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(createSubscriptionRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - createSubscriptionRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - /* Functional Consumer example using inner class */ - Consumer consumer = opcuaResponse -> { - try { - ExtensionObjectDefinition reply = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN),false).getBody(); - if (reply instanceof CreateSubscriptionResponse) { - CreateSubscriptionResponse responseMessage = (CreateSubscriptionResponse) reply; - - // Pass the response back to the application. - future.complete(responseMessage); - } else { - if (reply instanceof ServiceFault) { - ExtensionObjectDefinition header = ((ServiceFault) reply).getResponseHeader(); - LOGGER.error("Subscription request ended up with ServiceFault: {}", header); - future.completeExceptionally(new PlcRuntimeException( - String.format("Subscription request ended up with ServiceFault: %s", header) - )); - } else { - LOGGER.error("Remote party returned an error '{}'", reply); - future.completeExceptionally(new PlcRuntimeException( - String.format("Remote party returned an error '%s'", reply) - )); - } - } - } catch (ParseException e) { - LOGGER.error("error parsing", e); - } - }; - - /* Functional Consumer example using inner class */ - Consumer timeout = e -> { - LOGGER.error("Timeout while waiting on the crate subscription response", e); - // Pass the response back to the application. - future.completeExceptionally(e); - }; - - /* Functional Consumer example using inner class */ - BiConsumer error = (message, e) -> { - LOGGER.error("Error while creating the subscription", e); - // Pass the response back to the application. - future.completeExceptionally(e); - }; - - channel.submit(context, timeout, error, consumer, buffer); - } catch (SerializationException e) { - LOGGER.error("Error while creating the subscription", e); - future.completeExceptionally(e); - } - return future; + return conversation.submit(createSubscriptionRequest, CreateSubscriptionResponse.class); } @Override @@ -976,4 +846,14 @@ private GuidValue toGuidValue(String identifier) { byte[] data5 = new byte[]{0, 0, 0, 0, 0, 0}; return new GuidValue(0L, 0, 0, data4, data5); } + + private static void bridge(RequestTransaction transaction, CompletableFuture future, T response, Throwable error) { + if (error != null) { + future.completeExceptionally(error); + transaction.failRequest(error); + } else { + future.complete(response); + transaction.endRequest(); + } + } } 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 e90031be580..d7029b42d2e 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 @@ -19,21 +19,25 @@ package org.apache.plc4x.java.opcua.protocol; import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent; import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest; import org.apache.plc4x.java.api.model.PlcConsumerRegistration; import org.apache.plc4x.java.api.value.PlcValue; -import org.apache.plc4x.java.opcua.context.SecureChannel; +import org.apache.plc4x.java.opcua.context.Conversation; import org.apache.plc4x.java.opcua.tag.OpcuaTag; import org.apache.plc4x.java.opcua.readwrite.*; -import org.apache.plc4x.java.spi.ConversationContext; -import org.apache.plc4x.java.spi.generation.*; import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionEvent; import org.apache.plc4x.java.spi.messages.utils.ResponseItem; import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration; import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionTag; import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionHandle; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager; +import org.apache.plc4x.java.spi.transaction.RequestTransactionManager.RequestTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,49 +47,42 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiConsumer; import java.util.function.Consumer; public class OpcuaSubscriptionHandle extends DefaultPlcSubscriptionHandle { private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaSubscriptionHandle.class); + private final static ScheduledExecutorService EXECUTOR = newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "plc4x-opcua-subscription-scheduler")); + private final Set> consumers; private final List tagNames; - private final SecureChannel channel; + private final Conversation conversation; private final PlcSubscriptionRequest subscriptionRequest; - private final AtomicBoolean destroy = new AtomicBoolean(false); private final OpcuaProtocolLogic plcSubscriber; private final Long subscriptionId; private final long cycleTime; private final long revisedCycleTime; - private boolean complete = false; private final AtomicLong clientHandles = new AtomicLong(1L); + private final RequestTransactionManager tm; + private ScheduledFuture publishTask; - private final ConversationContext context; - - public OpcuaSubscriptionHandle(ConversationContext context, OpcuaProtocolLogic plcSubscriber, SecureChannel channel, PlcSubscriptionRequest subscriptionRequest, Long subscriptionId, long cycleTime) { + public OpcuaSubscriptionHandle(OpcuaProtocolLogic plcSubscriber, RequestTransactionManager tm, + Conversation conversation, PlcSubscriptionRequest subscriptionRequest, Long subscriptionId, long cycleTime) { super(plcSubscriber); + this.tm = tm; this.consumers = new HashSet<>(); this.subscriptionRequest = subscriptionRequest; this.tagNames = new ArrayList<>(subscriptionRequest.getTagNames()); - this.channel = channel; + this.conversation = conversation; this.subscriptionId = subscriptionId; this.plcSubscriber = plcSubscriber; this.cycleTime = cycleTime; this.revisedCycleTime = cycleTime; - this.context = context; - try { - onSubscribeCreateMonitoredItemsRequest().get(); - } catch (Exception e) { - LOGGER.info("Unable to serialize the Create Monitored Item Subscription Message", e); - plcSubscriber.onDisconnect(context); - } - startSubscriber(); } - private CompletableFuture onSubscribeCreateMonitoredItemsRequest() { + public CompletableFuture onSubscribeCreateMonitoredItemsRequest() { List requestList = new ArrayList<>(this.tagNames.size()); for (String tagName : this.tagNames) { final DefaultPlcSubscriptionTag tagDefaultPlcSubscription = (DefaultPlcSubscriptionTag) subscriptionRequest.getTag(tagName); @@ -129,16 +126,7 @@ private CompletableFuture onSubscribeCreateMonitor requestList.add(request); } - CompletableFuture future = new CompletableFuture<>(); - - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - channel.getRequestHandle(), - 0L, - OpcuaProtocolLogic.NULL_STRING, - SecureChannel.REQUEST_TIMEOUT_LONG, - OpcuaProtocolLogic.NULL_EXTENSION_OBJECT); - + RequestHeader requestHeader = conversation.createRequestHeader(); CreateMonitoredItemsRequest createMonitoredItemsRequest = new CreateMonitoredItemsRequest( requestHeader, subscriptionId, @@ -147,37 +135,14 @@ private CompletableFuture onSubscribeCreateMonitor requestList ); - ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(createMonitoredItemsRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - expandedNodeId, - null, - createMonitoredItemsRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - Consumer consumer = opcuaResponse -> { - CreateMonitoredItemsResponse responseMessage = null; - try { - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof CreateMonitoredItemsResponse) { - responseMessage = (CreateMonitoredItemsResponse) unknownExtensionObject; - } else { - ServiceFault serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString()); - plcSubscriber.onDisconnect(context); - } - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Subscription response", e); - plcSubscriber.onDisconnect(context); + return conversation.submit(createMonitoredItemsRequest, CreateMonitoredItemsResponse.class) + .whenComplete((response, error) -> { + if (error instanceof TimeoutException) { + LOGGER.info("Timeout while sending the Create Monitored Item Subscription Message", error); + } else if (error != null) { + LOGGER.info("Error while sending the Create Monitored Item Subscription Message", error); } + }).thenApply(responseMessage -> { MonitoredItemCreateResult[] array = responseMessage.getResults().toArray(new MonitoredItemCreateResult[0]); for (int index = 0, arrayLength = array.length; index < arrayLength; index++) { MonitoredItemCreateResult result = array[index]; @@ -187,155 +152,67 @@ private CompletableFuture onSubscribeCreateMonitor LOGGER.debug("Tag {} was added to the subscription", tagNames.get(index)); } } - future.complete(responseMessage); - }; - - Consumer timeout = e -> { - LOGGER.info("Timeout while sending the Create Monitored Item Subscription Message", e); - plcSubscriber.onDisconnect(context); - }; - - BiConsumer error = (message, e) -> { - LOGGER.info("Error while sending the Create Monitored Item Subscription Message", e); - plcSubscriber.onDisconnect(context); - }; - channel.submit(context, timeout, error, consumer, buffer); - - } catch (SerializationException e) { - LOGGER.info("Unable to serialize the Create Monitored Item Subscription Message", e); - plcSubscriber.onDisconnect(context); - } - return future; - } - - private void sleep(long length) { - try { - Thread.sleep(length); - } catch (InterruptedException e) { - LOGGER.trace("Interrupted Exception"); - } + LOGGER.trace("Scheduling publish event for subscription {}", subscriptionId); + publishTask = EXECUTOR.scheduleAtFixedRate(this::sendPublishRequest, revisedCycleTime / 2, revisedCycleTime, TimeUnit.MILLISECONDS); + return this; + }); } /** - * Main subscriber loop. For subscription we still need to send a request the server on every cycle. - * Which includes a request for an update of the previsouly agreed upon list of tags. + * Main subscriber loop. For subscription, we still need to send a request the server on every cycle. + * Which includes a request for an update of the previously agreed upon list of tags. * The server will respond at most once every cycle. * * @return */ - public void startSubscriber() { - LOGGER.trace("Starting Subscription"); - CompletableFuture.supplyAsync(() -> { - try { - LinkedList outstandingAcknowledgements = new LinkedList<>(); - List outstandingRequests = new LinkedList<>(); - while (!this.destroy.get()) { - long requestHandle = channel.getRequestHandle(); - - //If we are waiting on a response and haven't received one, just wait until we do. A keep alive will be sent out eventually - if (outstandingRequests.size() <= 1) { - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - requestHandle, - 0L, - OpcuaProtocolLogic.NULL_STRING, - this.revisedCycleTime * 10, - OpcuaProtocolLogic.NULL_EXTENSION_OBJECT); - - //Make a copy of the outstanding requests, so it isn't modified while we are putting the ack list together. - List acks = (LinkedList) outstandingAcknowledgements.clone(); - int ackLength = acks.size() == 0 ? -1 : acks.size(); - outstandingAcknowledgements.removeAll(acks); - - PublishRequest publishRequest = new PublishRequest( - requestHeader, - ackLength, - acks - ); - - ExpandedNodeId extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(publishRequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - extExpandedNodeId, - null, - publishRequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - - /* Create Consumer for the response message, error and timeout to be sent to the Secure Channel */ - Consumer consumer = opcuaResponse -> { - PublishResponse responseMessage = null; - ServiceFault serviceFault = null; - try { - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof PublishResponse) { - responseMessage = (PublishResponse) unknownExtensionObject; - } else { - serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.debug("Subscription ServiceFault returned from server with error code, '{}', ignoring as it is probably just a result of a Delete Subscription Request", header.getServiceResult().toString()); - //plcSubscriber.onDisconnect(context); - } - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Subscription response", e); - plcSubscriber.onDisconnect(context); - } - if (serviceFault == null) { - outstandingRequests.remove(((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle()); - - for (long availableSequenceNumber : responseMessage.getAvailableSequenceNumbers()) { - outstandingAcknowledgements.add(new SubscriptionAcknowledgement(this.subscriptionId, availableSequenceNumber)); - } - - for (ExtensionObject notificationMessage : ((NotificationMessage) responseMessage.getNotificationMessage()).getNotificationData()) { - ExtensionObjectDefinition notification = notificationMessage.getBody(); - if (notification instanceof DataChangeNotification) { - LOGGER.trace("Found a Data Change notification"); - List items = ((DataChangeNotification) notification).getMonitoredItems(); - onSubscriptionValue(items.toArray(new MonitoredItemNotification[0])); - } else { - LOGGER.warn("Unsupported Notification type"); - } - } - } - }; - - Consumer timeout = e -> { - LOGGER.error("Timeout while waiting for subscription response", e); - plcSubscriber.onDisconnect(context); - }; - - BiConsumer error = (message, e) -> { - LOGGER.error("Error while waiting for subscription response", e); - plcSubscriber.onDisconnect(context); - }; - - outstandingRequests.add(requestHandle); - channel.submit(context, timeout, error, consumer, buffer); - - } catch (SerializationException e) { - LOGGER.warn("Unable to serialize subscription request", e); + private void sendPublishRequest() { + List outstandingAcknowledgements = new LinkedList<>(); + List outstandingRequests = new LinkedList<>(); + + //If we are waiting on a response and haven't received one, just wait until we do. A keep alive will be sent out eventually + if (outstandingRequests.size() <= 1) { + RequestHeader requestHeader = conversation.createRequestHeader(this.revisedCycleTime * 10); + + //Make a copy of the outstanding requests, so it isn't modified while we are putting the ack list together. + List acks = new ArrayList<>(outstandingAcknowledgements); + int ackLength = acks.isEmpty() ? -1 : acks.size(); + outstandingAcknowledgements.removeAll(acks); + + PublishRequest publishRequest = new PublishRequest(requestHeader, ackLength, acks); + // we work in external thread - we need to coordinate access to conversation pipeline + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { + // Create Consumer for the response message, error and timeout to be sent to the Secure Channel + conversation.submit(publishRequest, PublishResponse.class).thenAccept(responseMessage -> { + outstandingRequests.remove(((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle()); + + for (long availableSequenceNumber : responseMessage.getAvailableSequenceNumbers()) { + outstandingAcknowledgements.add(new SubscriptionAcknowledgement(this.subscriptionId, availableSequenceNumber)); + } + + for (ExtensionObject notificationMessage : ((NotificationMessage) responseMessage.getNotificationMessage()).getNotificationData()) { + ExtensionObjectDefinition notification = notificationMessage.getBody(); + if (notification instanceof DataChangeNotification) { + LOGGER.trace("Found a Data Change notification"); + List items = ((DataChangeNotification) notification).getMonitoredItems(); + onSubscriptionValue(items.toArray(new MonitoredItemNotification[0])); + } else { + LOGGER.warn("Unsupported Notification type"); } } - /* Put the subscriber loop to sleep for the rest of the cycle. */ - sleep(this.revisedCycleTime); - } - //Wait for any outstanding responses to arrive, using the request timeout length - //sleep(this.revisedCycleTime * 10); - complete = true; - } catch (Exception e) { - LOGGER.error("Failed to start subscription", e); - } - return null; - }, - newSingleThreadExecutor()); + }).whenComplete((result, error) -> { + if (error != null) { + LOGGER.warn("Publish request of subscription {} resulted in error reported by server", subscriptionId, error); + transaction.failRequest(error); + } else { + LOGGER.trace("Completed publish request for subscription {}", subscriptionId); + transaction.endRequest(); + } + }); + outstandingRequests.add(requestHeader.getRequestHandle()); + }); + } } @@ -345,74 +222,29 @@ public void startSubscriber() { * @return */ public void stopSubscriber() { - this.destroy.set(true); - - long requestHandle = channel.getRequestHandle(); - - RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(), - SecureChannel.getCurrentDateTime(), - requestHandle, - 0L, - OpcuaProtocolLogic.NULL_STRING, - this.revisedCycleTime * 10, - OpcuaProtocolLogic.NULL_EXTENSION_OBJECT); - - List subscriptions = new ArrayList<>(1); - subscriptions.add(subscriptionId); - DeleteSubscriptionsRequest deleteSubscriptionrequest = new DeleteSubscriptionsRequest(requestHeader, + RequestHeader requestHeader = conversation.createRequestHeader(this.revisedCycleTime * 10); + List subscriptions = Collections.singletonList(subscriptionId); + DeleteSubscriptionsRequest deleteSubscriptionRequest = new DeleteSubscriptionsRequest(requestHeader, 1, subscriptions ); - ExpandedNodeId extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified - false, //Server Index Specified - new NodeIdFourByte((short) 0, Integer.parseInt(deleteSubscriptionrequest.getIdentifier())), - null, - null); - - ExtensionObject extObject = new ExtensionObject( - extExpandedNodeId, - null, - deleteSubscriptionrequest); - - try { - WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); - extObject.serialize(buffer); - + // subscription suspend can be invoked from multiple places, hence we manage transaction side of it + RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> { // Create Consumer for the response message, error and timeout to be sent to the Secure Channel - Consumer consumer = opcuaResponse -> { - DeleteSubscriptionsResponse responseMessage = null; - try { - ExtensionObjectDefinition unknownExtensionObject = ExtensionObject.staticParse(new ReadBufferByteBased(opcuaResponse, ByteOrder.LITTLE_ENDIAN), false).getBody(); - if (unknownExtensionObject instanceof DeleteSubscriptionsResponse) { - responseMessage = (DeleteSubscriptionsResponse) unknownExtensionObject; + conversation.submit(deleteSubscriptionRequest, DeleteSubscriptionsResponse.class) + .thenAccept(responseMessage -> publishTask.cancel(true)) + .whenComplete((result, error) -> { + if (error != null) { + LOGGER.error("Deletion of subscription resulted in error", error); + transaction.failRequest(error); } else { - ServiceFault serviceFault = (ServiceFault) unknownExtensionObject; - ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader(); - LOGGER.debug("Fault when deleting Subscription ServiceFault return from server with error code, '{}', ignoring as it is probably just a result of a Delete Subscription Request", header.getServiceResult().toString()); + transaction.endRequest(); } - } catch (ParseException e) { - LOGGER.error("Unable to parse the returned Delete Subscriptions Response", e); - } - }; - - Consumer timeout = e -> { - LOGGER.error("Timeout while waiting for delete subscription response", e); - plcSubscriber.onDisconnect(context); - }; - - BiConsumer error = (message, e) -> { - LOGGER.error("Error while waiting for delete subscription response", e); - plcSubscriber.onDisconnect(context); - }; - - channel.submit(context, timeout, error, consumer, buffer); - } catch (SerializationException e) { - LOGGER.warn("Unable to serialize subscription request", e); - } - - sleep(500); - plcSubscriber.removeSubscription(subscriptionId); + plcSubscriber.removeSubscription(subscriptionId); + }); + }); } /** @@ -446,4 +278,8 @@ public PlcConsumerRegistration register(Consumer consumer) return new DefaultPlcConsumerRegistration(plcSubscriber, consumer, this); } + public Long getSubscriptionId() { + return subscriptionId; + } + } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/Chunk.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/Chunk.java new file mode 100644 index 00000000000..a09840ae987 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/Chunk.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +import java.util.Objects; + +public class Chunk { + + private final int securityHeaderSize; + private final int cipherTextBlockSize; + private final int plainTextBlockSize; + private final int signatureSize; + private final int maxChunkSize; + private final int paddingOverhead; + private final int maxCipherTextSize; + private final int maxCipherTextBlocks; + private final int maxPlainTextSize; + private final int maxBodySize; + + private boolean asymmetric; + private boolean encrypted; + private boolean signed; + + public Chunk(int securityHeaderSize, int cipherTextBlockSize, int plainTextBlockSize, int signatureSize, int maxChunkSize) { + this(securityHeaderSize, cipherTextBlockSize, plainTextBlockSize, signatureSize, maxChunkSize, false, false, false); + } + + public Chunk(int securityHeaderSize, int cipherTextBlockSize, int plainTextBlockSize, int signatureSize, int maxChunkSize, + boolean asymmetric, boolean encrypted, boolean signed) { + this.securityHeaderSize = securityHeaderSize; + this.cipherTextBlockSize = cipherTextBlockSize; + this.plainTextBlockSize = plainTextBlockSize; + this.signatureSize = signatureSize; + this.maxChunkSize = maxChunkSize; + this.asymmetric = asymmetric; + this.encrypted = encrypted; + this.signed = signed; + this.maxCipherTextSize = maxChunkSize - 12 /* security header */ - securityHeaderSize; + this.maxCipherTextBlocks = maxCipherTextSize / cipherTextBlockSize; + this.paddingOverhead = cipherTextBlockSize > 256 ? 2 : (cipherTextBlockSize < 16 ? 0 : 1); + this.maxPlainTextSize = maxCipherTextBlocks * plainTextBlockSize; + this.maxBodySize = maxPlainTextSize - 8 /* sequence header */ - this.paddingOverhead - signatureSize; + } + + public int getSecurityHeaderSize() { + return securityHeaderSize; + } + public int getCipherTextBlockSize() { + return cipherTextBlockSize; + } + public int getPlainTextBlockSize() { + return plainTextBlockSize; + } + public int getSignatureSize() { + return signatureSize; + } + public int getMaxChunkSize() { + return maxChunkSize; + } + public int getPaddingOverhead() { + return paddingOverhead; + } + public int getMaxCipherTextSize() { + return maxCipherTextSize; + } + public int getMaxCipherTextBlocks() { + return maxCipherTextBlocks; + } + public int getMaxPlainTextSize() { + return maxPlainTextSize; + } + public int getMaxBodySize() { + return maxBodySize; + } + + public boolean isAsymmetric() { + return asymmetric; + } + + public boolean isEncrypted() { + return encrypted; + } + + public boolean isSigned() { + return signed; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Chunk)) { + return false; + } + Chunk chunk = (Chunk) o; + return getSecurityHeaderSize() == chunk.getSecurityHeaderSize() + && getCipherTextBlockSize() == chunk.getCipherTextBlockSize() + && getPlainTextBlockSize() == chunk.getPlainTextBlockSize() + && getSignatureSize() == chunk.getSignatureSize() + && getMaxChunkSize() == chunk.getMaxChunkSize() + && getPaddingOverhead() == chunk.getPaddingOverhead() + && getMaxCipherTextSize() == chunk.getMaxCipherTextSize() + && getMaxCipherTextBlocks() == chunk.getMaxCipherTextBlocks() + && getMaxPlainTextSize() == chunk.getMaxPlainTextSize() + && getMaxBodySize() == chunk.getMaxBodySize(); + } + + @Override + public int hashCode() { + return Objects.hash(getSecurityHeaderSize(), getCipherTextBlockSize(), + getPlainTextBlockSize(), + getSignatureSize(), getMaxChunkSize(), getPaddingOverhead(), getMaxCipherTextSize(), + getMaxCipherTextBlocks(), getMaxPlainTextSize(), getMaxBodySize()); + } + + @Override + public String toString() { + return "Chunk" + + "{ securityHeaderSize=" + securityHeaderSize + + ", cipherTextBlockSize=" + cipherTextBlockSize + + ", plainTextBlockSize=" + plainTextBlockSize + + ", signatureSize=" + signatureSize + + ", maxChunkSize=" + maxChunkSize + + ", paddingOverhead=" + paddingOverhead + + ", maxCipherTextSize=" + maxCipherTextSize + + ", maxCipherTextBlocks=" + maxCipherTextBlocks + + ", maxPlainTextSize=" + maxPlainTextSize + + ", maxBodySize=" + maxBodySize + + '}'; + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactory.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactory.java new file mode 100644 index 00000000000..a86f514956f --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactory.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +import io.vavr.control.Try; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.plc4x.java.opcua.context.Conversation; +import org.apache.plc4x.java.opcua.readwrite.OpcuaProtocolLimits; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; + +public class ChunkFactory { + + public static int SYMMETRIC_SECURITY_HEADER_SIZE = 4; + + public Chunk create(boolean asymmetric, Conversation conversation) { + return create(asymmetric, + conversation.isSymmetricEncryptionEnabled(), + conversation.isSymmetricSigningEnabled(), + conversation.getSecurityPolicy(), + conversation.getLimits(), + conversation.getLocalCertificate(), + conversation.getRemoteCertificate() + ); + } + + public Chunk create(boolean asymmetric, boolean encrypted, boolean signed, SecurityPolicy securityPolicy, + OpcuaProtocolLimits limits, X509Certificate localCertificate, X509Certificate remoteCertificate) { + + if (securityPolicy == SecurityPolicy.NONE) { + return new Chunk( + asymmetric ? 59 : SYMMETRIC_SECURITY_HEADER_SIZE, + 1, + 1, + securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + false, + false + ); + } + + // asymmetric messages are always signed and encrypted, however non-asymmetric messages + // exchanged after handshake might have message security mode set to NONE which results + // in no overhead to communication + boolean encryption = asymmetric || encrypted; + boolean signing = asymmetric || signed; + + int localAsymmetricKeyLength = asymmetric ? keySize(localCertificate) : 0; + int remoteAsymmetricKeyLength = asymmetric ? keySize(remoteCertificate) : 0; + int localCertificateSize = asymmetric ? certificateBytes(localCertificate).length : 0; + int serverCertificateThumbprint = asymmetric ? certificateThumbprint(remoteCertificate).length : 0; + + int asymmetricSecurityHarderSize = (12 + securityPolicy.getSecurityPolicyUri().length() + localCertificateSize + serverCertificateThumbprint); + int asymmetricCipherTextBlockSize = asymmetric ? (localAsymmetricKeyLength + 7) / 8 : 0; + int plainTextTextBlockSize = asymmetric ? (localAsymmetricKeyLength + 7) / 8 : 0; + + int cipherTextBlockSize = asymmetric ? asymmetricCipherTextBlockSize : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1); + + if (securityPolicy == SecurityPolicy.Basic128Rsa15) { + // 12 + 56 + 674 + 20 + return new Chunk( + asymmetric ? asymmetricSecurityHarderSize : SYMMETRIC_SECURITY_HEADER_SIZE, + cipherTextBlockSize, + asymmetric ? plainTextTextBlockSize - 11 : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1), + asymmetric ? ((remoteAsymmetricKeyLength + 7) / 8) : securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + encryption, + signing + ); + } else if (securityPolicy == SecurityPolicy.Basic256) { + return new Chunk( + // 12 + 56 + 674 + 20 + asymmetric ? asymmetricSecurityHarderSize : SYMMETRIC_SECURITY_HEADER_SIZE, + cipherTextBlockSize, + asymmetric ? plainTextTextBlockSize - 42 : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1), + asymmetric ? ((remoteAsymmetricKeyLength + 7) / 8) : securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + encryption, + signing + ); + } else if (securityPolicy == SecurityPolicy.Basic256Sha256) { + return new Chunk( + asymmetric ? asymmetricSecurityHarderSize : SYMMETRIC_SECURITY_HEADER_SIZE, + cipherTextBlockSize, + asymmetric ? plainTextTextBlockSize - 42 : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1), + asymmetric ? ((remoteAsymmetricKeyLength + 7) / 8) : securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + encryption, + signing + ); + } else if (securityPolicy == SecurityPolicy.Aes128_Sha256_RsaOaep) { + return new Chunk( + asymmetric ? asymmetricSecurityHarderSize : SYMMETRIC_SECURITY_HEADER_SIZE, + cipherTextBlockSize, + asymmetric ? plainTextTextBlockSize - 42 : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1), + asymmetric ? ((remoteAsymmetricKeyLength + 7) / 8) : securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + encryption, + signing + ); + } else if (securityPolicy == SecurityPolicy.Aes256_Sha256_RsaPss) { + return new Chunk( + asymmetric ? asymmetricSecurityHarderSize : SYMMETRIC_SECURITY_HEADER_SIZE, + cipherTextBlockSize, + asymmetric ? plainTextTextBlockSize - 66 : (encrypted ? securityPolicy.getEncryptionBlockSize() : 1), + asymmetric ? ((remoteAsymmetricKeyLength + 7) / 8) : securityPolicy.getSymmetricSignatureSize(), + (int) limits.getSendBufferSize(), + asymmetric, + encryption, + signing + ); + } + + throw new IllegalArgumentException("Unsupported security policy " + securityPolicy.name() + "[" + securityPolicy.getSecurityPolicyUri() + "]"); + } + + private static int keySize(X509Certificate certificate) { + PublicKey publicKey = certificate != null ? certificate.getPublicKey() : null; + + return (publicKey instanceof RSAPublicKey) ? ((RSAPublicKey) publicKey).getModulus().bitLength() : 0; + } + + private static byte[] certificateThumbprint(X509Certificate certificate) { + return DigestUtils.sha1(certificateBytes(certificate)); + } + + private static byte[] certificateBytes(X509Certificate certificate) { + return Try.of(() -> certificate.getEncoded()).getOrElse(new byte[0]); + } + + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java new file mode 100644 index 00000000000..a34e50fc251 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +public interface ChunkStorage { + + /** + * Appends segmented frame. + * + * @param frame Segmented frame. + */ + void append(byte[] frame); + + /** + * Gets accumulated size of stored data. + * + * @return Occupied memory in bytes. + */ + long size(); + + /** + * Retrieves final result from segmented payload. + * + * @return Assembled result. + */ + byte[] get(); + + void reset(); +} \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java new file mode 100644 index 00000000000..d5b36feb821 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ +package org.apache.plc4x.java.opcua.protocol.chunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class MemoryChunkStorage implements ChunkStorage { + + private final List chunks = new ArrayList<>(); + private long size = 0; + + @Override + public void append(byte[] frame) { + chunks.add(frame); + size += chunks.get(chunks.size() - 1).length; + } + + public long size() { + return size; + } + + @Override + public byte[] get() { + Optional collect = chunks.stream().reduce((b1, b2) -> { + byte[] combined = new byte[b1.length + b2.length]; + System.arraycopy(b1, 0, combined, 0, b1.length); + System.arraycopy(b2, 0, combined, b1.length, b2.length); + return combined; + }); + return collect.orElse(new byte[0]); + } + + @Override + public void reset() { + chunks.clear(); + size = 0; + } + + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverter.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverter.java new file mode 100644 index 00000000000..c84d8589218 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverter.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +import java.nio.ByteBuffer; +import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; +import org.apache.plc4x.java.opcua.readwrite.ExtensiblePayload; +import org.apache.plc4x.java.opcua.readwrite.ExtensionObject; +import org.apache.plc4x.java.opcua.readwrite.MessagePDU; +import org.apache.plc4x.java.opcua.readwrite.Payload; +import org.apache.plc4x.java.spi.generation.ByteOrder; +import org.apache.plc4x.java.spi.generation.Message; +import org.apache.plc4x.java.spi.generation.ParseException; +import org.apache.plc4x.java.spi.generation.ReadBufferByteBased; +import org.apache.plc4x.java.spi.generation.SerializationException; +import org.apache.plc4x.java.spi.generation.WriteBufferByteBased; + +public class PayloadConverter { + + public static BinaryPayload toBinary(Payload payload) throws SerializationException { + if (payload instanceof BinaryPayload) { + return (BinaryPayload) payload; + } + + return toBinary((ExtensiblePayload) payload); + } + + + public static BinaryPayload toBinary(ExtensiblePayload extensible) throws SerializationException { + ExtensionObject payload = extensible.getPayload(); + + WriteBufferByteBased buffer = new WriteBufferByteBased(payload.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); + payload.serialize(buffer); + + return new BinaryPayload(extensible.getSequenceHeader(), buffer.getBytes()); + } + + public static ExtensiblePayload toExtensible(BinaryPayload binary) throws ParseException { + byte[] payload = binary.getPayload(); + + ReadBufferByteBased buffer = new ReadBufferByteBased(payload, ByteOrder.LITTLE_ENDIAN); + ExtensionObject extensionObject = ExtensionObject.staticParse(buffer, false); + + return new ExtensiblePayload(binary.getSequenceHeader(), extensionObject); + } + + public static byte[] toStream(Payload payload) throws SerializationException { + return serialize(payload); + } + + public static byte[] toStream(MessagePDU apdu) throws SerializationException { + return serialize(apdu); + } + + private static byte[] serialize(Message message) throws SerializationException { + WriteBufferByteBased buffer = new WriteBufferByteBased(message.getLengthInBytes(), ByteOrder.LITTLE_ENDIAN); + message.serialize(buffer); + + return buffer.getBytes(); + } + + public static Payload fromStream(byte[] payload, boolean extensible) throws ParseException { + ReadBufferByteBased buffer = new ReadBufferByteBased(payload, ByteOrder.LITTLE_ENDIAN); + return Payload.staticParse(buffer, extensible, (long) (extensible ? -1 : payload.length - 8)); + } + + public static MessagePDU fromStream(ByteBuffer chunkBuffer, boolean response, boolean encrypted) throws ParseException { + ReadBufferByteBased buffer = new ReadBufferByteBased(chunkBuffer.array(), ByteOrder.LITTLE_ENDIAN); + return MessagePDU.staticParse(buffer, response, encrypted); + } + + public static MessagePDU pduFromStream(byte[] message, boolean response) throws ParseException { + ReadBufferByteBased buffer = new ReadBufferByteBased(message, ByteOrder.LITTLE_ENDIAN); + return MessagePDU.staticParse(buffer, response); + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/MessageSecurity.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/MessageSecurity.java new file mode 100644 index 00000000000..418b5f4f8d4 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/MessageSecurity.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.security; + +import org.apache.plc4x.java.opcua.readwrite.MessageSecurityMode; + +public enum MessageSecurity { + + NONE (MessageSecurityMode.messageSecurityModeNone), + SIGN (MessageSecurityMode.messageSecurityModeSign), + SIGN_ENCRYPT (MessageSecurityMode.messageSecurityModeSignAndEncrypt); + + private final MessageSecurityMode mode; + + MessageSecurity(MessageSecurityMode mode) { + this.mode = mode; + } + + public MessageSecurityMode getMode() { + return mode; + } + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SecurityPolicy.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SecurityPolicy.java index c910a6c29d0..73b868b831c 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SecurityPolicy.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SecurityPolicy.java @@ -1,3 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ package org.apache.plc4x.java.opcua.security; import javax.crypto.Cipher; @@ -9,24 +27,52 @@ public enum SecurityPolicy { NONE("http://opcfoundation.org/UA/SecurityPolicy#None", - new MacSignatureAlgorithm("", 0, 32), + new MacSignatureAlgorithm(""), new EncryptionAlgorithm(""), new SignatureAlgorithm("", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"), new EncryptionAlgorithm(""), - 1), + 0, 0, 0, 1, 0 + ), Basic128Rsa15("http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15", - new MacSignatureAlgorithm("HmacSHA1", 20, 16), + new MacSignatureAlgorithm("HmacSHA1"), new EncryptionAlgorithm("AES/CBC/NoPadding"), new SignatureAlgorithm("SHA1withRSA", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"), new EncryptionAlgorithm("RSA/ECB/PKCS1Padding"), - 11), + 20, 16, 16, 16, 16 + ), + + Basic256("http://opcfoundation.org/UA/SecurityPolicy#Basic256", + new MacSignatureAlgorithm("HmacSHA1"), + new EncryptionAlgorithm("AES/CBC/NoPadding"), + new SignatureAlgorithm("SHA1withRSA", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"), + new EncryptionAlgorithm("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), + 20, 24, 32, 16, 32 + ), + Basic256Sha256("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256", - new MacSignatureAlgorithm("HmacSHA256", 32, 32), + new MacSignatureAlgorithm("HmacSHA256"), new EncryptionAlgorithm("AES/CBC/NoPadding"), new SignatureAlgorithm("SHA256withRSA", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"), new EncryptionAlgorithm("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), - 42); + 32, 32, 32, 16, 32 + ), + + Aes128_Sha256_RsaOaep("http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep", + new MacSignatureAlgorithm("HmacSHA256"), + new EncryptionAlgorithm("AES/CBC/NoPadding"), + new SignatureAlgorithm("SHA256withRSA", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"), + new EncryptionAlgorithm("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), + 32, 32, 16, 16, 32 + ), + + Aes256_Sha256_RsaPss("http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss", + new MacSignatureAlgorithm("HmacSHA256"), + new EncryptionAlgorithm("AES/CBC/NoPadding"), + new SignatureAlgorithm("SHA256withRSA/PSS", "http://opcfoundation.org/UA/security/rsa-pss-sha2-256"), + new EncryptionAlgorithm("RSA/ECB/OAEPWithSHA256AndMGF1Padding"), + 32, 32, 32, 16, 32 + ); private final String securityPolicyUri; @@ -36,20 +82,31 @@ public enum SecurityPolicy { private final EncryptionAlgorithm symmetricEncryptionAlgorithm; private final SignatureAlgorithm asymmetricSignatureAlgorithm; private final EncryptionAlgorithm asymmetricEncryptionAlgorithm; - private final int asymmetricPlainBlock; + private final int symmetricSignatureSize; + private final int signatureKeySize; + private final int encryptionKeySize; + private final int encryptionBlockSize; + private final int nonceLength; SecurityPolicy(String securityPolicyUri, - MacSignatureAlgorithm symmetricSignatureAlgorithm, - EncryptionAlgorithm symmetricEncryptionAlgorithm, - SignatureAlgorithm asymmetricSignatureAlgorithm, - EncryptionAlgorithm asymmetricEncryptionAlgorithm, - int asymmetricPlainBlock) { + MacSignatureAlgorithm symmetricSignatureAlgorithm, + EncryptionAlgorithm symmetricEncryptionAlgorithm, + SignatureAlgorithm asymmetricSignatureAlgorithm, + EncryptionAlgorithm asymmetricEncryptionAlgorithm, + int symmetricSignatureSize, + int signatureKeySize, int encryptionKeySize, + int encryptionBlockSize, int nonceLength + ) { this.securityPolicyUri = securityPolicyUri; this.symmetricSignatureAlgorithm = symmetricSignatureAlgorithm; this.symmetricEncryptionAlgorithm = symmetricEncryptionAlgorithm; this.asymmetricSignatureAlgorithm = asymmetricSignatureAlgorithm; this.asymmetricEncryptionAlgorithm = asymmetricEncryptionAlgorithm; - this.asymmetricPlainBlock = asymmetricPlainBlock; + this.symmetricSignatureSize = symmetricSignatureSize; + this.signatureKeySize = signatureKeySize; + this.encryptionKeySize = encryptionKeySize; + this.encryptionBlockSize = encryptionBlockSize; + this.nonceLength = nonceLength; } public static SecurityPolicy findByName(String securityPolicy) { @@ -79,21 +136,33 @@ public EncryptionAlgorithm getSymmetricEncryptionAlgorithm() { return symmetricEncryptionAlgorithm; } - public int getAsymmetricPlainBlock() { - return asymmetricPlainBlock; + public int getSymmetricSignatureSize() { + return symmetricSignatureSize; + } + + public int getSignatureKeySize() { + return signatureKeySize; + } + + public int getEncryptionKeySize() { + return encryptionKeySize; + } + + public int getEncryptionBlockSize() { + return encryptionBlockSize; + } + + public int getNonceLength() { + return nonceLength; } public static class MacSignatureAlgorithm { private final String name; - private final int symmetricSignatureSize; - private final int keySize; - MacSignatureAlgorithm(String name, int symmetricSignatureSize, int keySize) { + MacSignatureAlgorithm(String name) { this.name = name; - this.symmetricSignatureSize = symmetricSignatureSize; - this.keySize = keySize; } public Mac getSignature() throws NoSuchAlgorithmException { @@ -104,13 +173,6 @@ public String getName() { return name; } - public int getSymmetricSignatureSize() { - return symmetricSignatureSize; - } - - public int getKeySize() { - return keySize; - } } diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SymmetricKeys.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SymmetricKeys.java index f0457128b0f..253aa7d64a1 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SymmetricKeys.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/SymmetricKeys.java @@ -1,5 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ package org.apache.plc4x.java.opcua.security; +import java.util.Arrays; import org.apache.plc4x.java.opcua.security.SecurityPolicy.MacSignatureAlgorithm; import javax.crypto.Mac; @@ -9,40 +28,56 @@ public class SymmetricKeys { private final Keys clientKeys; + private final byte[] senderNonce; private final Keys serverKeys; + private final byte[] receiverNonce; - public SymmetricKeys(Keys clientKeys, Keys serverKeys) { + public SymmetricKeys(Keys clientKeys, byte[] senderNonce, Keys serverKeys, byte[] receiverNonce) { this.clientKeys = clientKeys; + this.senderNonce = senderNonce; this.serverKeys = serverKeys; + this.receiverNonce = receiverNonce; } public Keys getClientKeys() { return clientKeys; } + public byte[] getSenderNonce() { + return senderNonce; + } + public Keys getServerKeys() { return serverKeys; } - public static SymmetricKeys generateKeyPair(byte[] clientNonce, byte[] serverNonce, MacSignatureAlgorithm policy) { - int signatureKeySize = policy.getKeySize(); - int encryptionKeySize = policy.getKeySize(); - int cipherTextBlockSize = 16; + public byte[] getReceiverNonce() { + return receiverNonce; + } + // make sure that keys are + public boolean matches(byte[] senderNonce, byte[] receiverNonce) { + return Arrays.equals(this.senderNonce, senderNonce) && Arrays.equals(this.receiverNonce, receiverNonce); + } - byte[] clientSignatureKey = createKey(serverNonce, clientNonce, 0, signatureKeySize, policy); - byte[] clientEncryptionKey = createKey(serverNonce, clientNonce, signatureKeySize, encryptionKeySize, policy); - byte[] clientInitializationVector = createKey(serverNonce, clientNonce, signatureKeySize + encryptionKeySize, cipherTextBlockSize, policy); + public static SymmetricKeys generateKeyPair(byte[] senderNonce, byte[] receiverNonce, SecurityPolicy securityPolicy) { + int signatureKeySize = securityPolicy.getSignatureKeySize(); + int encryptionKeySize = securityPolicy.getEncryptionKeySize(); + int cipherTextBlockSize = securityPolicy.getEncryptionBlockSize(); + MacSignatureAlgorithm policy = securityPolicy.getSymmetricSignatureAlgorithm(); + byte[] senderSignatureKey = createKey(receiverNonce, senderNonce, 0, signatureKeySize, policy); + byte[] senderEncryptionKey = createKey(receiverNonce, senderNonce, signatureKeySize, encryptionKeySize, policy); + byte[] senderInitializationVector = createKey(receiverNonce, senderNonce, signatureKeySize + encryptionKeySize, cipherTextBlockSize, policy); - byte[] serverSignatureKey = createKey(clientNonce, serverNonce, 0, signatureKeySize, policy); - byte[] serverEncryptionKey = createKey(clientNonce, serverNonce, signatureKeySize, encryptionKeySize, policy); - byte[] serverInitializationVector = createKey(clientNonce, serverNonce, signatureKeySize + encryptionKeySize, cipherTextBlockSize, policy); + byte[] receiverSignatureKey = createKey(senderNonce, receiverNonce, 0, signatureKeySize, policy); + byte[] receiverEncryptionKey = createKey(senderNonce, receiverNonce, signatureKeySize, encryptionKeySize, policy); + byte[] receiverInitializationVector = createKey(senderNonce, receiverNonce, signatureKeySize + encryptionKeySize, cipherTextBlockSize, policy); return new SymmetricKeys( - new Keys(clientSignatureKey, clientEncryptionKey, clientInitializationVector), - new Keys(serverSignatureKey, serverEncryptionKey, serverInitializationVector) + new Keys(senderSignatureKey, senderEncryptionKey, senderInitializationVector), senderNonce, + new Keys(receiverSignatureKey, receiverEncryptionKey, receiverInitializationVector), receiverNonce ); } diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaDriverIT.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaDriverIT.java index e9342841a84..cbd3885d249 100644 --- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaDriverIT.java +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaDriverIT.java @@ -21,7 +21,6 @@ import org.apache.plc4x.test.driver.DriverTestsuiteRunner; import org.junit.jupiter.api.Disabled; -@Disabled("Fails due to mapping errors") public class OpcuaDriverIT extends DriverTestsuiteRunner { public OpcuaDriverIT() { diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java index 2c2a4e5f2ae..b834dcad2ee 100644 --- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java @@ -18,9 +18,11 @@ */ package org.apache.plc4x.java.opcua; -import io.vavr.collection.List; +import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -29,6 +31,7 @@ import org.apache.plc4x.java.api.PlcConnectionManager; import org.apache.plc4x.java.api.PlcDriverManager; import org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication; +import org.apache.plc4x.java.api.exceptions.PlcUnsupportedDataTypeException; import org.apache.plc4x.java.api.messages.PlcReadRequest; import org.apache.plc4x.java.api.messages.PlcReadResponse; import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent; @@ -37,13 +40,16 @@ import org.apache.plc4x.java.api.messages.PlcWriteRequest; import org.apache.plc4x.java.api.messages.PlcWriteResponse; import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.opcua.security.MessageSecurity; import org.apache.plc4x.java.opcua.security.SecurityPolicy; import org.apache.plc4x.java.opcua.tag.OpcuaTag; import org.assertj.core.api.Condition; +import org.assertj.core.api.SoftAssertions; import org.eclipse.milo.examples.server.TestMiloServer; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +60,9 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Stream; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; public class OpcuaPlcDriverTest { @@ -76,6 +84,7 @@ public class OpcuaPlcDriverTest { private static final String UINT64_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInt64"; private static final String UINTEGER_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInteger"; private static final String DOES_NOT_EXIST_IDENTIFIER_READ_WRITE = "ns=2;i=12512623"; + private static final String DOES_NOT_EXISTS_TAG_NAME = "DoesNotExists"; // tag name // At the moment not used in PLC4X or in the OPC UA driver private static final String BYTE_STRING_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/ByteString"; @@ -229,7 +238,7 @@ public void manySubscriptionsOnSingleConnection() throws Exception { class ConnectionRelated { @TestFactory Stream connectionNoParams() { - return connectionStringValidSet.toStream() + return connectionStringValidSet.stream() .map(connectionString -> DynamicTest.dynamicTest(connectionString, () -> { PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(connectionString); Condition is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); @@ -237,15 +246,14 @@ Stream connectionNoParams() { opcuaConnection.close(); assertThat(opcuaConnection).isNot(is_connected); })) - .map(DynamicNode.class::cast) - .toJavaStream(); + .map(DynamicNode.class::cast); } @TestFactory Stream connectionWithDiscoveryParam() throws Exception { - return connectionStringValidSet.toStream() + return connectionStringValidSet.stream() .map(connectionAddress -> DynamicContainer.dynamicContainer(connectionAddress, () -> - discoveryParamValidSet.toStream().map(discoveryParam -> DynamicTest.dynamicTest(discoveryParam, () -> { + discoveryParamValidSet.stream().map(discoveryParam -> DynamicTest.dynamicTest(discoveryParam, () -> { String connectionString = connectionAddress + paramSectionDivider + discoveryParam; PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(connectionString); Condition is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); @@ -255,8 +263,7 @@ Stream connectionWithDiscoveryParam() throws Exception { })) .map(DynamicNode.class::cast) .iterator())) - .map(DynamicNode.class::cast) - .toJavaStream(); + .map(DynamicNode.class::cast); } @Test @@ -315,162 +322,111 @@ void connectionWithPlcAuthenticationOverridesUrlParam() throws Exception { @Nested class readWrite { - + Map> tags = Map.ofEntries( + entry("Bool", entry(BOOL_IDENTIFIER_READ_WRITE, true)), + entry("Byte", entry(BYTE_IDENTIFIER_READ_WRITE + ";BYTE", (short) 3)), + entry("Double", entry(DOUBLE_IDENTIFIER_READ_WRITE, 0.5d)), + entry("Float", entry(FLOAT_IDENTIFIER_READ_WRITE, 0.5f)), + entry("Int16", entry(INT16_IDENTIFIER_READ_WRITE + ";INT", 1)), + entry("Int32", entry(INT32_IDENTIFIER_READ_WRITE, 42)), + entry("Int64", entry(INT64_IDENTIFIER_READ_WRITE, 42L)), + entry("Integer", entry(INTEGER_IDENTIFIER_READ_WRITE, -127)), + //entry("SByte", entry(SBYTE_IDENTIFIER_READ_WRITE, )), + entry("String", entry(STRING_IDENTIFIER_READ_WRITE, "Hello Toddy!")), + entry("UInt16", entry(UINT16_IDENTIFIER_READ_WRITE + ";UINT", 65535)), + entry("UInt32", entry(UINT32_IDENTIFIER_READ_WRITE + ";UDINT", 101010101L)), + entry("UInt64", entry(UINT64_IDENTIFIER_READ_WRITE + ";ULINT", new BigInteger("1337"))), + entry("UInteger", entry(UINTEGER_IDENTIFIER_READ_WRITE + ";UDINT", 102020202L)), + entry("BooleanArray", entry(BOOL_ARRAY_IDENTIFIER, new boolean[]{true, true, true, true, true})), + // entry("ByteStringArray", entry(BYTE_STRING_ARRAY_IDENTIFIER, null)), + entry("ByteArray", entry(BYTE_ARRAY_IDENTIFIER + ";BYTE", new Short[]{1, 100, 100, 255, 123})), + entry("DoubleArray", entry(DOUBLE_ARRAY_IDENTIFIER, new Double[]{1.0, 2.0, 3.0, 4.0, 5.0})), + entry("FloatArray", entry(FLOAT_ARRAY_IDENTIFIER, new Float[]{1.0F, 2.0F, 3.0F, 4.0F, 5.0F})), + entry("Int16Array", entry(INT16_ARRAY_IDENTIFIER, new Short[]{1, 2, 3, 4, 5})), + entry("Int32Array", entry(INT32_ARRAY_IDENTIFIER, new Integer[]{1, 2, 3, 4, 5})), + entry("Int64Array", entry(INT64_ARRAY_IDENTIFIER, new Long[]{1L, 2L, 3L, 4L, 5L})), + entry("IntegerArray", entry(INT32_ARRAY_IDENTIFIER, new Integer[]{1, 2, 3, 4, 5})), + entry("SByteArray", entry(SBYTE_ARRAY_IDENTIFIER, new Byte[]{1, 2, 3, 4, 5})), + entry("StringArray", entry(STRING_ARRAY_IDENTIFIER, new String[]{"1", "2", "3", "4", "5"})), + entry("UInt16Array", entry(UINT16_ARRAY_IDENTIFIER + ";UINT", new Short[]{1, 2, 3, 4, 5})), + entry("UInt32Array", entry(UINT32_ARRAY_IDENTIFIER + ";UDINT", new Integer[]{1, 2, 3, 4, 5})), + entry("UInt64Array", entry(UINT64_ARRAY_IDENTIFIER + ";ULINT", new Long[]{1L, 2L, 3L, 4L, 5L})), + entry(DOES_NOT_EXISTS_TAG_NAME, entry(DOES_NOT_EXIST_IDENTIFIER_READ_WRITE, "11")) + ); @ParameterizedTest - @EnumSource(SecurityPolicy.class) - public void readVariables(SecurityPolicy policy) throws Exception { - String connectionString = getConnectionString(policy); + @MethodSource("org.apache.plc4x.java.opcua.OpcuaPlcDriverTest#getConnectionSecurityPolicies") + public void readVariables(SecurityPolicy policy, MessageSecurity messageSecurity) throws Exception { + String connectionString = getConnectionString(policy, messageSecurity); PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(connectionString); Condition is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); assertThat(opcuaConnection).is(is_connected); - PlcReadRequest.Builder builder = opcuaConnection.readRequestBuilder() - .addTagAddress("Bool", BOOL_IDENTIFIER_READ_WRITE) - .addTagAddress("Byte", BYTE_IDENTIFIER_READ_WRITE) - .addTagAddress("Double", DOUBLE_IDENTIFIER_READ_WRITE) - .addTagAddress("Float", FLOAT_IDENTIFIER_READ_WRITE) - .addTagAddress("Int16", INT16_IDENTIFIER_READ_WRITE) - .addTagAddress("Int32", INT32_IDENTIFIER_READ_WRITE) - .addTagAddress("Int64", INT64_IDENTIFIER_READ_WRITE) - .addTagAddress("Integer", INTEGER_IDENTIFIER_READ_WRITE) - .addTagAddress("SByte", SBYTE_IDENTIFIER_READ_WRITE) - .addTagAddress("String", STRING_IDENTIFIER_READ_WRITE) - .addTagAddress("UInt16", UINT16_IDENTIFIER_READ_WRITE) - .addTagAddress("UInt32", UINT32_IDENTIFIER_READ_WRITE) - .addTagAddress("UInt64", UINT64_IDENTIFIER_READ_WRITE) - .addTagAddress("UInteger", UINTEGER_IDENTIFIER_READ_WRITE) - - .addTagAddress("BoolArray", BOOL_ARRAY_IDENTIFIER) - //.addTagAddress("ByteStringArray", BYTE_STRING_ARRAY_IDENTIFIER); - .addTagAddress("ByteArray", BYTE_ARRAY_IDENTIFIER) - .addTagAddress("DoubleArray", DOUBLE_ARRAY_IDENTIFIER) - .addTagAddress("FloatArray", FLOAT_ARRAY_IDENTIFIER) - .addTagAddress("Int16Array", INT16_ARRAY_IDENTIFIER) - .addTagAddress("Int32Array", INT32_ARRAY_IDENTIFIER) - .addTagAddress("Int64Array", INT64_ARRAY_IDENTIFIER) - .addTagAddress("SByteArray", SBYTE_ARRAY_IDENTIFIER) - .addTagAddress("StringArray", STRING_ARRAY_IDENTIFIER) - .addTagAddress("UInt16Array", UINT16_ARRAY_IDENTIFIER) - .addTagAddress("UInt32Array", UINT32_ARRAY_IDENTIFIER) - .addTagAddress("UInt64Array", UINT64_ARRAY_IDENTIFIER) - - .addTagAddress("DoesNotExists", DOES_NOT_EXIST_IDENTIFIER_READ_WRITE); - + PlcReadRequest.Builder builder = opcuaConnection.readRequestBuilder(); + tags.forEach((tagName, tagEntry) -> builder.addTagAddress(tagName, tagEntry.getKey())); PlcReadRequest request = builder.build(); PlcReadResponse response = request.execute().get(); - List.of( - "Bool", - "Byte", - "Double", - "Float", - "Int16", - "Int32", - "Int64", - "Integer", - "SByte", - "String", - "UInt16", - "UInt32", - "UInt64", - "UInteger", - "BoolArray", - "ByteArray", - "DoubleArray", - "FloatArray", - "Int16Array", - "Int32Array", - "Int64Array", - "SByteArray", - "StringArray", - "UInt16Array", - "UInt32Array", - "UInt64Array" - ).forEach(tag -> assertThat(response.getResponseCode(tag)).isEqualTo(PlcResponseCode.OK)); - - - assertThat(response.getResponseCode("DoesNotExists")).isEqualTo(PlcResponseCode.NOT_FOUND); + + SoftAssertions softly = new SoftAssertions(); + tags.keySet().forEach(tag -> { + if (DOES_NOT_EXISTS_TAG_NAME.equals(tag)) { + softly.assertThat(response.getResponseCode(tag)) + .describedAs("Tag %s should not exist and return NOT_FOUND status", tag) + .isEqualTo(PlcResponseCode.NOT_FOUND); + } else { + softly.assertThat(response.getResponseCode(tag)) + .describedAs("Tag %s should exist and return OK status", tag) + .isEqualTo(PlcResponseCode.OK); + } + }); + softly.assertAll(); opcuaConnection.close(); assertThat(opcuaConnection.isConnected()).isFalse(); } @ParameterizedTest - @EnumSource(SecurityPolicy.class) - public void writeVariables(SecurityPolicy policy) throws Exception { - - PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(getConnectionString(policy)); + @MethodSource("org.apache.plc4x.java.opcua.OpcuaPlcDriverTest#getConnectionSecurityPolicies") + public void writeVariables(SecurityPolicy policy, MessageSecurity messageSecurity) throws Exception { + String connectionString = getConnectionString(policy, messageSecurity); + PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(connectionString); Condition is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); assertThat(opcuaConnection).is(is_connected); - PlcWriteRequest.Builder builder = opcuaConnection.writeRequestBuilder() - .addTagAddress("Bool", BOOL_IDENTIFIER_READ_WRITE, true) - .addTagAddress("Byte", BYTE_IDENTIFIER_READ_WRITE + ";BYTE", (short) 3) - .addTagAddress("Double", DOUBLE_IDENTIFIER_READ_WRITE, 0.5d) - .addTagAddress("Float", FLOAT_IDENTIFIER_READ_WRITE, 0.5f) - //.addTagAddress("Int16", INT16_IDENTIFIER_READ_WRITE + "", (short) 1) - .addTagAddress("Int32", INT32_IDENTIFIER_READ_WRITE, 42) - .addTagAddress("Int64", INT64_IDENTIFIER_READ_WRITE, 42L) - .addTagAddress("Integer", INTEGER_IDENTIFIER_READ_WRITE, 42) - .addTagAddress("SByte", SBYTE_IDENTIFIER_READ_WRITE + ";SINT", -127) - .addTagAddress("String", STRING_IDENTIFIER_READ_WRITE, "Helllo Toddy!") - .addTagAddress("UInt16", UINT16_IDENTIFIER_READ_WRITE + ";UINT", 65535) - .addTagAddress("UInt32", UINT32_IDENTIFIER_READ_WRITE + ";UDINT", 101010101L) - .addTagAddress("UInt64", UINT64_IDENTIFIER_READ_WRITE + ";ULINT", new BigInteger("1337")) - .addTagAddress("UInteger", UINTEGER_IDENTIFIER_READ_WRITE + ";UDINT", 102020202L) - - - .addTagAddress("BooleanArray", BOOL_ARRAY_IDENTIFIER, (Object[]) new Boolean[]{true, true, true, true, true}) - .addTagAddress("ByteArray", BYTE_ARRAY_IDENTIFIER + ";BYTE", (Object[]) new Short[]{1, 100, 100, 255, 123}) - .addTagAddress("DoubleArray", DOUBLE_ARRAY_IDENTIFIER, (Object[]) new Double[]{1.0, 2.0, 3.0, 4.0, 5.0}) - .addTagAddress("FloatArray", FLOAT_ARRAY_IDENTIFIER, (Object[]) new Float[]{1.0F, 2.0F, 3.0F, 4.0F, 5.0F}) - .addTagAddress("Int16Array", INT16_ARRAY_IDENTIFIER, (Object[]) new Short[]{1, 2, 3, 4, 5}) - .addTagAddress("Int32Array", INT32_ARRAY_IDENTIFIER, (Object[]) new Integer[]{1, 2, 3, 4, 5}) - .addTagAddress("Int64Array", INT64_ARRAY_IDENTIFIER, (Object[]) new Long[]{1L, 2L, 3L, 4L, 5L}) - .addTagAddress("IntegerArray", INT32_ARRAY_IDENTIFIER, (Object[]) new Integer[]{1, 2, 3, 4, 5}) - .addTagAddress("SByteArray", SBYTE_ARRAY_IDENTIFIER, (Object[]) new Byte[]{1, 2, 3, 4, 5}) - .addTagAddress("StringArray", STRING_ARRAY_IDENTIFIER, (Object[]) new String[]{"1", "2", "3", "4", "5"}) - .addTagAddress("UInt16Array", UINT16_ARRAY_IDENTIFIER + ";UINT", (Object[]) new Short[]{1, 2, 3, 4, 5}) - .addTagAddress("UInt32Array", UINT32_ARRAY_IDENTIFIER + ";UDINT", (Object[]) new Integer[]{1, 2, 3, 4, 5}) - .addTagAddress("UInt64Array", UINT64_ARRAY_IDENTIFIER + ";ULINT", (Object[]) new Long[]{1L, 2L, 3L, 4L, 5L}) - - .addTagAddress("DoesNotExists", DOES_NOT_EXIST_IDENTIFIER_READ_WRITE, "11"); - + PlcWriteRequest.Builder builder = opcuaConnection.writeRequestBuilder(); + tags.forEach((tagName, tagEntry) -> { + System.out.println("Write tag " + tagName); + try { + Object value = tagEntry.getValue(); + if (value.getClass().isArray()) { + Object[] values = new Object[Array.getLength(value)]; + for (int index = 0; index < Array.getLength(value); index++) { + values[index] = Array.get(value, index); + } + builder.addTagAddress(tagName, tagEntry.getKey(), values); + } else { + builder.addTagAddress(tagName, tagEntry.getKey(), value); + } + } catch (PlcUnsupportedDataTypeException e) { + fail(e.toString()); + } + }); PlcWriteRequest request = builder.build(); PlcWriteResponse response = request.execute().get(); - List.of( - "Bool", - "Byte", - "Double", - "Float", - //"Int16", // TODO: why is htat disabled??? - "Int32", - "Int64", - "Integer", - "SByte", - "String", - "UInt16", - "UInt32", - "UInt64", - "UInteger", - "BooleanArray", - "ByteArray", - "DoubleArray", - "FloatArray", - "Int16Array", - "Int32Array", - "Int64Array", - "IntegerArray", - "SByteArray", - "StringArray", - "UInt16Array", - "UInt32Array", - "UInt64Array" - ).forEach(s -> { - assertThat(response.getResponseCode(s)).withFailMessage(s + "is not ok").isEqualTo(PlcResponseCode.OK); + SoftAssertions softly = new SoftAssertions(); + tags.keySet().forEach(tag -> { + if (DOES_NOT_EXISTS_TAG_NAME.equals(tag)) { + softly.assertThat(response.getResponseCode(DOES_NOT_EXISTS_TAG_NAME)) + .describedAs("Tag %s should not exist and return NOT_FOUND status", tag) + .isEqualTo(PlcResponseCode.NOT_FOUND); + } else { + softly.assertThat(response.getResponseCode(tag)) + .describedAs("Tag %s should exist and return OK status", tag) + .isEqualTo(PlcResponseCode.OK); + } }); - assertThat(response.getResponseCode("DoesNotExists")).isEqualTo(PlcResponseCode.NOT_FOUND); + softly.assertAll(); opcuaConnection.close(); assert !opcuaConnection.isConnected(); @@ -555,21 +511,26 @@ public void run() { assert !opcuaConnection.isConnected(); } - private String getConnectionString(SecurityPolicy policy) { + private String getConnectionString(SecurityPolicy policy, MessageSecurity messageSecurity) { switch (policy) { case NONE: return tcpConnectionAddress; + + case Basic256: case Basic128Rsa15: case Basic256Sha256: + case Aes128_Sha256_RsaOaep: + case Aes256_Sha256_RsaPss: Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "server"); String keyStoreFile = securityTempDir.resolve("security").resolve("example-server.pfx").toAbsolutePath().toString(); String certDirectory = securityTempDir.toAbsolutePath().toString(); String connectionParams = Stream.of( - Map.entry("keyStoreFile", keyStoreFile), - Map.entry("certDirectory", certDirectory), - Map.entry("keyStorePassword", "password"), - Map.entry("securityPolicy", policy) + entry("keyStoreFile", keyStoreFile), + entry("certDirectory", certDirectory), + entry("keyStorePassword", "password"), + entry("securityPolicy", policy.name()), + entry("messageSecurity", messageSecurity.name()) ) .map(tuple -> tuple.getKey() + "=" + tuple.getValue()) .collect(Collectors.joining(paramDivider)); @@ -580,4 +541,27 @@ private String getConnectionString(SecurityPolicy policy) { throw new IllegalStateException(); } } + + private static Stream getConnectionSecurityPolicies() { + return Stream.of( + Arguments.of(SecurityPolicy.NONE, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.NONE, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.NONE, MessageSecurity.SIGN_ENCRYPT), + Arguments.of(SecurityPolicy.Basic256Sha256, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.Basic256Sha256, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.Basic256Sha256, MessageSecurity.SIGN_ENCRYPT), + Arguments.of(SecurityPolicy.Basic256, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.Basic256, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.Basic256, MessageSecurity.SIGN_ENCRYPT), + Arguments.of(SecurityPolicy.Basic128Rsa15, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.Basic128Rsa15, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.Basic128Rsa15, MessageSecurity.SIGN_ENCRYPT), + Arguments.of(SecurityPolicy.Aes128_Sha256_RsaOaep, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.Aes128_Sha256_RsaOaep, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.Aes128_Sha256_RsaOaep, MessageSecurity.SIGN_ENCRYPT), + Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurity.NONE), + Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurity.SIGN), + Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurity.SIGN_ENCRYPT) + ); + } } diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/TestCertificateGenerator.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/TestCertificateGenerator.java new file mode 100644 index 00000000000..a633479cb91 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/TestCertificateGenerator.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +public class TestCertificateGenerator { + + public static Entry generate(int keySize, String dn, long validitySec) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(keySize, new SecureRandom()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder( + new X500Principal(dn), + BigInteger.valueOf(new Random().nextLong()), + new Date(), + new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(validitySec)), + new X500Principal(dn), + keyPair.getPublic() + ); + X509CertificateHolder cert = certGen.build(new JcaContentSignerBuilder("SHA256withRSA") + .build(keyPair.getPrivate())); + + X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(cert); + return Map.entry(keyPair.getPrivate(), certificate); + } catch (CertificateException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new RuntimeException("Could not initialize test - certificate generation failed"); + } + } + +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/EncryptionHandlerTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/EncryptionHandlerTest.java new file mode 100644 index 00000000000..901ba9f943f --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/EncryptionHandlerTest.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.context; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map.Entry; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.plc4x.java.opcua.TestCertificateGenerator; +import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; +import org.apache.plc4x.java.opcua.readwrite.ChunkType; +import org.apache.plc4x.java.opcua.readwrite.MessagePDU; +import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenRequest; +import org.apache.plc4x.java.opcua.readwrite.OpcuaProtocolLimits; +import org.apache.plc4x.java.opcua.readwrite.OpenChannelMessageRequest; +import org.apache.plc4x.java.opcua.readwrite.PascalByteString; +import org.apache.plc4x.java.opcua.readwrite.PascalString; +import org.apache.plc4x.java.opcua.readwrite.Payload; +import org.apache.plc4x.java.opcua.readwrite.SecurityHeader; +import org.apache.plc4x.java.opcua.readwrite.SequenceHeader; +import org.apache.plc4x.java.opcua.security.MessageSecurity; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class EncryptionHandlerTest { + + Supplier sequenceSupplier = () -> 0; + + CertificateKeyPair clientKeyPair; + CertificateKeyPair serverKeyPair; + + @BeforeEach + public void setUp() throws Exception { + Entry clientKeyPair = TestCertificateGenerator.generate(2048, "cn=client", 3600); + Entry serverKeyPair = TestCertificateGenerator.generate(2048, "cn=server", 3600); + + X509Certificate clientCertificate = clientKeyPair.getValue(); + PublicKey clientPublicKey = clientCertificate.getPublicKey(); + this.clientKeyPair = new CertificateKeyPair(new KeyPair(clientPublicKey, clientKeyPair.getKey()), clientCertificate); + + X509Certificate serverCertificate = serverKeyPair.getValue(); + PublicKey serverPublicKey = serverCertificate.getPublicKey(); + this.serverKeyPair = new CertificateKeyPair(new KeyPair(clientPublicKey, serverKeyPair.getKey()), serverCertificate); + } + + @Test + void testAsymmetricEncryption() throws Exception { + Conversation conversation = createSecureChannel(clientKeyPair.getCertificate(), serverKeyPair.getCertificate(), + SecurityPolicy.Basic128Rsa15, MessageSecurity.SIGN_ENCRYPT, true, true + ); + + EncryptionHandler handler = new EncryptionHandler(conversation, clientKeyPair.getKeyPair().getPrivate()); + + int[] messageSizes = {128}; + for (int messageSize : messageSizes) { + byte[] messageBytes = new byte[messageSize]; + for (int i = 0; i < messageBytes.length; i++) { + messageBytes[i] = (byte) i; + } + + SecurityHeader securityHeader = new SecurityHeader(0, 1); + SequenceHeader sequenceHeader = new SequenceHeader(1, 1); + BinaryPayload payload = new BinaryPayload(sequenceHeader, messageBytes); + + OpcuaOpenRequest request = new OpcuaOpenRequest(ChunkType.FINAL, + new OpenChannelMessageRequest( + (int) securityHeader.getSecureChannelId(), + new PascalString(SecurityPolicy.Basic128Rsa15.getSecurityPolicyUri()), + stringFromBytes(clientKeyPair.getCertificate().getEncoded()), + stringFromBytes(DigestUtils.sha1(serverKeyPair.getCertificate().getEncoded())) + ), + payload + ); + List pdus = handler.encodeMessage( + request, sequenceSupplier + ); + assertEquals(1, pdus.size()); + + // decrypt + conversation = createSecureChannel(serverKeyPair.getCertificate(), clientKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN_ENCRYPT, true, true); + EncryptionHandler decrypter = new EncryptionHandler(conversation, serverKeyPair.getPrivateKey()); + MessagePDU decoded = decrypter.decodeMessage(pdus.get(0)); + assertTrue(decoded instanceof OpcuaOpenRequest); + OpcuaOpenRequest decodedRequest = (OpcuaOpenRequest) decoded; + SequenceHeader decodedSequenceHeader = decodedRequest.getMessage().getSequenceHeader(); + Payload decodedPayload = decodedRequest.getMessage(); + assertEquals(sequenceHeader.getSequenceNumber(), decodedSequenceHeader.getSequenceNumber()); + assertEquals(sequenceHeader.getRequestId(), decodedSequenceHeader.getRequestId()); + assertArrayEquals(messageBytes, ((BinaryPayload) decodedPayload).getPayload()); + } + + } + + @Test + void testAsymmetricEncryptionSign() throws Exception { + Conversation secureChannel = createSecureChannel(clientKeyPair.getCertificate(), serverKeyPair.getCertificate(), + SecurityPolicy.Basic128Rsa15, MessageSecurity.SIGN, true, true); + + EncryptionHandler handler = new EncryptionHandler(secureChannel, clientKeyPair.getPrivateKey()); + + int[] messageSizes = {128}; + for (int messageSize : messageSizes) { + byte[] messageBytes = new byte[messageSize]; + for (int i = 0; i < messageBytes.length; i++) { + messageBytes[i] = (byte) i; + } + + SecurityHeader securityHeader = new SecurityHeader(0, 1); + SequenceHeader sequenceHeader = new SequenceHeader(1, 1); + BinaryPayload payload = new BinaryPayload(sequenceHeader, messageBytes); + + OpcuaOpenRequest request = new OpcuaOpenRequest(ChunkType.FINAL, + new OpenChannelMessageRequest( + (int) securityHeader.getSecureChannelId(), + new PascalString(SecurityPolicy.Basic128Rsa15.getSecurityPolicyUri()), + stringFromBytes(clientKeyPair.getCertificate().getEncoded()), + stringFromBytes(DigestUtils.sha1(serverKeyPair.getCertificate().getEncoded())) + ), + payload + ); + List pdus = handler.encodeMessage( + request, sequenceSupplier + ); + assertEquals(1, pdus.size()); + + // decrypt + secureChannel = createSecureChannel(serverKeyPair.getCertificate(), clientKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN, true, true); + EncryptionHandler decryptHandler = new EncryptionHandler(secureChannel, serverKeyPair.getPrivateKey()); + MessagePDU decoded = decryptHandler.decodeMessage(pdus.get(0)); + OpcuaOpenRequest decodedRequest = (OpcuaOpenRequest) decoded; + SequenceHeader decodedSequenceHeader = decodedRequest.getMessage().getSequenceHeader(); + Payload decodedPayload = decodedRequest.getMessage(); + assertEquals(sequenceHeader.getSequenceNumber(), decodedSequenceHeader.getSequenceNumber()); + assertEquals(sequenceHeader.getRequestId(), decodedSequenceHeader.getRequestId()); + assertArrayEquals(messageBytes, ((BinaryPayload) decodedPayload).getPayload()); + } + + } + + @Test + void testSymmetricEncryption() throws Exception { + Conversation secureChannel = createSecureChannel(clientKeyPair.getCertificate(), serverKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN_ENCRYPT, true, true); + + EncryptionHandler handler = new EncryptionHandler(secureChannel, clientKeyPair.getPrivateKey()); + + int[] messageSizes = {128}; + for (int messageSize : messageSizes) { + byte[] messageBytes = new byte[messageSize]; + for (int i = 0; i < messageBytes.length; i++) { + messageBytes[i] = (byte) i; + } + + SecurityHeader securityHeader = new SecurityHeader(0, 1); + SequenceHeader sequenceHeader = new SequenceHeader(1, 1); + BinaryPayload payload = new BinaryPayload(sequenceHeader, messageBytes); + + OpcuaMessageRequest request = new OpcuaMessageRequest(ChunkType.FINAL, + securityHeader, + payload + ); + List pdus = handler.encodeMessage( + request, sequenceSupplier + ); + assertEquals(1, pdus.size()); + + // decrypt + secureChannel = createSecureChannel(serverKeyPair.getCertificate(), clientKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN, true, true); + EncryptionHandler decryptHandler = new EncryptionHandler(secureChannel, serverKeyPair.getPrivateKey()); + MessagePDU decoded = decryptHandler.decodeMessage(pdus.get(0)); + OpcuaMessageRequest decodedRequest = (OpcuaMessageRequest) decoded; + SequenceHeader decodedSequenceHeader = decodedRequest.getMessage().getSequenceHeader(); + Payload decodedPayload = decodedRequest.getMessage(); + assertEquals(sequenceHeader.getSequenceNumber(), decodedSequenceHeader.getSequenceNumber()); + assertEquals(sequenceHeader.getRequestId(), decodedSequenceHeader.getRequestId()); + assertArrayEquals(messageBytes, ((BinaryPayload) decodedPayload).getPayload()); + } + } + + @Test + void testSymmetricEncryptionSign() throws Exception { + Conversation secureChannel = createSecureChannel(clientKeyPair.getCertificate(), serverKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN, true, true); + + EncryptionHandler handler = new EncryptionHandler(secureChannel, clientKeyPair.getPrivateKey()); + + int[] messageSizes = {128}; + for (int messageSize : messageSizes) { + byte[] messageBytes = new byte[messageSize]; + for (int i = 0; i < messageBytes.length; i++) { + messageBytes[i] = (byte) i; + } + + SecurityHeader securityHeader = new SecurityHeader(0, 1); + SequenceHeader sequenceHeader = new SequenceHeader(1, 1); + BinaryPayload payload = new BinaryPayload(sequenceHeader, messageBytes); + + OpcuaMessageRequest request = new OpcuaMessageRequest(ChunkType.FINAL, + securityHeader, + payload + ); + List pdus = handler.encodeMessage( + request, sequenceSupplier + ); + assertEquals(1, pdus.size()); + + // decrypt + secureChannel = createSecureChannel(serverKeyPair.getCertificate(), clientKeyPair.getCertificate(), SecurityPolicy.Basic128Rsa15, + MessageSecurity.SIGN, true, true); + EncryptionHandler decryptHandler = new EncryptionHandler(secureChannel, serverKeyPair.getPrivateKey()); + MessagePDU decoded = decryptHandler.decodeMessage(pdus.get(0)); + OpcuaMessageRequest decodedRequest = (OpcuaMessageRequest) decoded; + SequenceHeader decodedSequenceHeader = decodedRequest.getMessage().getSequenceHeader(); + Payload decodedPayload = decodedRequest.getMessage(); + assertEquals(sequenceHeader.getSequenceNumber(), decodedSequenceHeader.getSequenceNumber()); + assertEquals(sequenceHeader.getRequestId(), decodedSequenceHeader.getRequestId()); + assertArrayEquals(messageBytes, ((BinaryPayload) decodedPayload).getPayload()); + } + } + + private static PascalByteString stringFromBytes(byte[] bytes) { + return new PascalByteString(bytes.length, bytes); + } + + private static Conversation createSecureChannel(X509Certificate localCertificate, X509Certificate remoteCertificate, SecurityPolicy securityPolicy, + MessageSecurity messageSecurity, boolean encrypted, boolean signed) { + OpcuaProtocolLimits limits = new OpcuaProtocolLimits(8196, 8196, 8196 * 10, 10); + Conversation conversation = Mockito.mock(Conversation.class); + when(conversation.getLimits()).thenReturn(limits); + when(conversation.getLocalCertificate()).thenReturn(localCertificate); + when(conversation.getRemoteCertificate()).thenReturn(remoteCertificate); + when(conversation.getSecurityPolicy()).thenReturn(securityPolicy); + when(conversation.getMessageSecurity()).thenReturn(messageSecurity); + when(conversation.isSymmetricEncryptionEnabled()).thenReturn(encrypted); + when(conversation.isSymmetricSigningEnabled()).thenReturn(signed); + when(conversation.getLocalNonce()).thenReturn(new byte[32]); + when(conversation.getRemoteNonce()).thenReturn(new byte[32]); + return conversation; + } + +} \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java index 143703f5693..c9bdb43676b 100644 --- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java @@ -18,16 +18,22 @@ */ package org.apache.plc4x.java.opcua.protocol; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; import org.apache.plc4x.java.DefaultPlcDriverManager; import org.apache.plc4x.java.api.PlcConnection; import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest; import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse; import org.apache.plc4x.java.api.types.PlcResponseCode; import org.apache.plc4x.java.opcua.OpcuaPlcDriverTest; +import org.apache.plc4x.java.opcua.readwrite.Argument; import org.apache.plc4x.test.DisableInDockerFlag; import org.apache.plc4x.test.DisableOnParallelsVmFlag; import org.eclipse.milo.examples.server.ExampleServer; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +43,10 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; // ! For some odd reason does this test not work on VMs running in Parallels. // cdutz: I have done way more than my fair share on tracking down this issue and am simply giving up on it. @@ -110,317 +120,9 @@ public static void tearDown() throws Exception { // ! If this test fails, see comment at the top of the class before investigating. @Test - public void subscribeBool() throws Exception { - String tag = "Bool"; - String identifier = BOOL_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeByte() throws Exception { - String tag = "Byte"; - String identifier = BYTE_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeDouble() throws Exception { - String tag = "Double"; - String identifier = DOUBLE_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeFloat() throws Exception { - String tag = "Float"; - String identifier = FLOAT_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeInt16() throws Exception { - String tag = "Int16"; - String identifier = INT16_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeInt32() throws Exception { - String tag = "Int32"; - String identifier = INT32_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeInt64() throws Exception { - String tag = "Int64"; - String identifier = INT64_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeInteger() throws Exception { - String tag = "Integer"; - String identifier = INTEGER_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeSByte() throws Exception { - String tag = "SByte"; - String identifier = SBYTE_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeString() throws Exception { - String tag = "String"; - String identifier = STRING_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeUInt16() throws Exception { - String tag = "Uint16"; - String identifier = UINT16_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); - } - - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeUInt32() throws Exception { - String tag = "UInt32"; - String identifier = UINT32_IDENTIFIER_READ_WRITE; + public void subscribeDoesNotExists() throws Exception { + String tag = "DoesNotExists"; + String identifier = DOES_NOT_EXIST_IDENTIFIER_READ_WRITE; LOGGER.info("Starting subscription {} test", tag); // Create Subscription @@ -434,8 +136,8 @@ public void subscribeUInt32() throws Exception { // Create handler for returned value subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); + //This should never be called, + fail("Received subscription response whereas error was expected"); }); //Wait for value to be returned from server @@ -446,24 +148,27 @@ public void subscribeUInt32() throws Exception { // ! If this test fails, see comment at the top of the class before investigating. @Test - public void subscribeUInt64() throws Exception { - String tag = "UInt64"; - String identifier = UINT64_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); + public void subscribeMultiple() throws Exception { + String tag1 = "UInteger"; + String identifier1 = UINTEGER_IDENTIFIER_READ_WRITE; + String tag2 = "Integer"; + String identifier2 = INTEGER_IDENTIFIER_READ_WRITE; + LOGGER.info("Starting subscription {} and {} test", tag1, tag2); // Create Subscription PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); + builder.addChangeOfStateTagAddress(tag1, identifier1); + builder.addChangeOfStateTagAddress(tag2, identifier2); PlcSubscriptionRequest request = builder.build(); // Get result of creating subscription PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); + final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag1); // Create handler for returned value subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); + assert plcSubscriptionEvent.getResponseCode(tag1).equals(PlcResponseCode.OK); + assert plcSubscriptionEvent.getResponseCode(tag2).equals(PlcResponseCode.OK); }); //Wait for value to be returned from server @@ -474,24 +179,27 @@ public void subscribeUInt64() throws Exception { // ! If this test fails, see comment at the top of the class before investigating. @Test - public void subscribeUInteger() throws Exception { - String tag = "UInteger"; - String identifier = UINTEGER_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} test", tag); + public void subscribeMultipleWithOneMissing() throws Exception { + String tag1 = "UInteger"; + String identifier1 = UINTEGER_IDENTIFIER_READ_WRITE; + String tag2 = "Integer"; + String identifier2 = UINTEGER_IDENTIFIER_READ_WRITE + "_MISSING_GONE"; + LOGGER.info("Starting subscription {} and {} test", tag1, tag2); // Create Subscription PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); + builder.addChangeOfStateTagAddress(tag1, identifier1); + builder.addChangeOfStateTagAddress(tag2, identifier2); PlcSubscriptionRequest request = builder.build(); // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); + PlcSubscriptionResponse response = request.execute().get(10000, TimeUnit.MILLISECONDS); + final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag1); // Create handler for returned value subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag).equals(PlcResponseCode.OK); - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); + assert plcSubscriptionEvent.getResponseCode(tag1).equals(PlcResponseCode.OK); + assert plcSubscriptionEvent.getResponseCode(tag2).equals(PlcResponseCode.NOT_FOUND); }); //Wait for value to be returned from server @@ -500,64 +208,52 @@ public void subscribeUInteger() throws Exception { subscriptionHandle.stopSubscriber(); } - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeDoesNotExists() throws Exception { - String tag = "DoesNotExists"; - String identifier = DOES_NOT_EXIST_IDENTIFIER_READ_WRITE; + @ParameterizedTest + @MethodSource("getTags") + public void subscribeTest(String tag, Class type) throws Exception { LOGGER.info("Starting subscription {} test", tag); // Create Subscription PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag, identifier); + builder.addChangeOfStateTagAddress(tag, tag); PlcSubscriptionRequest request = builder.build(); // Get result of creating subscription PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag); + CountDownLatch latch = new CountDownLatch(1); // Create handler for returned value subscriptionHandle.register(plcSubscriptionEvent -> { - //This should never be called, - assert false; - LOGGER.info("Received a response from {} test {}", tag, plcSubscriptionEvent.getPlcValue(tag).toString()); + Object value = plcSubscriptionEvent.getObject(tag); + LOGGER.info("Received a response from {} test {} ({})", tag, plcSubscriptionEvent.getPlcValue(tag).toString(), value.getClass()); + assertEquals(PlcResponseCode.OK, plcSubscriptionEvent.getResponseCode(tag)); + assertNotNull(value); + assertTrue(type.isInstance(value)); + latch.countDown(); }); - //Wait for value to be returned from server - Thread.sleep(1200); - + assertTrue(latch.await(1200, TimeUnit.MILLISECONDS)); subscriptionHandle.stopSubscriber(); } - // ! If this test fails, see comment at the top of the class before investigating. - @Test - public void subscribeMultiple() throws Exception { - String tag1 = "UInteger"; - String identifier1 = UINTEGER_IDENTIFIER_READ_WRITE; - String tag2 = "Integer"; - String identifier2 = INTEGER_IDENTIFIER_READ_WRITE; - LOGGER.info("Starting subscription {} and {} test", tag1, tag2); - - // Create Subscription - PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder(); - builder.addChangeOfStateTagAddress(tag1, identifier1); - builder.addChangeOfStateTagAddress(tag2, identifier2); - PlcSubscriptionRequest request = builder.build(); - - // Get result of creating subscription - PlcSubscriptionResponse response = request.execute().get(1000, TimeUnit.MILLISECONDS); - final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(tag1); - - // Create handler for returned value - subscriptionHandle.register(plcSubscriptionEvent -> { - assert plcSubscriptionEvent.getResponseCode(tag1).equals(PlcResponseCode.OK); - assert plcSubscriptionEvent.getResponseCode(tag2).equals(PlcResponseCode.OK); - }); - - //Wait for value to be returned from server - Thread.sleep(1200); - - subscriptionHandle.stopSubscriber(); + private static Stream getTags() { + return Stream.of( + Arguments.of(BOOL_IDENTIFIER_READ_WRITE, Boolean.class), + Arguments.of(BYTE_IDENTIFIER_READ_WRITE, Short.class), + Arguments.of(DOUBLE_IDENTIFIER_READ_WRITE, Double.class), + Arguments.of(FLOAT_IDENTIFIER_READ_WRITE, Float.class), + Arguments.of(INT16_IDENTIFIER_READ_WRITE, Short.class), + Arguments.of(INT32_IDENTIFIER_READ_WRITE, Integer.class), + Arguments.of(INT64_IDENTIFIER_READ_WRITE, Long.class), + Arguments.of(INTEGER_IDENTIFIER_READ_WRITE, Integer.class), + Arguments.of(SBYTE_IDENTIFIER_READ_WRITE, byte[].class), + Arguments.of(STRING_IDENTIFIER_READ_WRITE, String.class), + Arguments.of(UINT16_IDENTIFIER_READ_WRITE, Integer.class), + Arguments.of(UINT32_IDENTIFIER_READ_WRITE, Long.class), + Arguments.of(UINT64_IDENTIFIER_READ_WRITE, Long.class), + Arguments.of(UINTEGER_IDENTIFIER_READ_WRITE, Long.class) + ); } } diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactoryTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactoryTest.java new file mode 100644 index 00000000000..78835405e9b --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkFactoryTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import javax.security.auth.x500.X500Principal; +import org.apache.plc4x.java.opcua.TestCertificateGenerator; +import org.apache.plc4x.java.opcua.readwrite.MessageSecurityMode; +import org.apache.plc4x.java.opcua.readwrite.OpcuaProtocolLimits; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; + +class ChunkFactoryTest { + + public static final Map> CERTIFICATES = new HashMap<>(); + + private OpcuaProtocolLimits limits = new OpcuaProtocolLimits( + 8196, + 8196, + 8196 * 10, + 10 + ); + + @ParameterizedTest + @CsvFileSource(numLinesToSkip = 1, resources = { + "/chunk-calculation-1024.csv", + "/chunk-calculation-2048.csv", + "/chunk-calculation-3072.csv", + "/chunk-calculation-1024.csv", + "/chunk-calculation-5120.csv" + }) + public void testChunkCalculation( + int keySize, + String securityPolicy, + String messageSecurity, + boolean asymmetric, + boolean encrypted, + boolean signed, + int securityHeaderSize, + int cipherTextBlockSize, + int plainTextBlockSize, + int signatureSize, + int maxChunkSize, + int paddingOverhead, + int maxCipherTextSize, + int maxCipherTextBlocks, + int maxPlainTextSize, + int maxBodySize + ) throws Exception { + verify(get(keySize), + securityPolicy, + messageSecurity, + asymmetric, + encrypted, + signed, + securityHeaderSize, + cipherTextBlockSize, + plainTextBlockSize, + signatureSize, + maxChunkSize, + paddingOverhead, + maxCipherTextSize, + maxCipherTextBlocks, + maxPlainTextSize, + maxBodySize + ); + } + + private void verify(Entry certificateEntry, String securityPolicy, String messageSecurity, + boolean asymmetric, boolean encrypted, boolean signed, + int securityHeaderSize, int cipherTextBlockSize, int plainTextBlockSize, int signatureSize, + int maxChunkSize, int paddingOverhead, int maxCipherTextSize, int maxCipherTextBlocks, int maxPlainTextSize, int maxBodySize) { + SecurityPolicy channelSecurityPolicy = null; + try { + channelSecurityPolicy = SecurityPolicy.valueOf(securityPolicy); + } catch (IllegalArgumentException e) { + Assumptions.abort("Unsupported security policy " + securityPolicy); + } + MessageSecurityMode channelMessageSecurity = null; + try { + channelMessageSecurity = MessageSecurityMode.valueOf(messageSecurity); + } catch (IllegalArgumentException e) { + Assumptions.abort("Unsupported security policy " + securityPolicy); + } + + ChunkFactory chunkFactory = new ChunkFactory(); + Chunk chunk = chunkFactory.create( + asymmetric, encrypted, signed, + channelSecurityPolicy, + limits, + certificateEntry.getValue(), + certificateEntry.getValue() + ); + + assertEquals(securityHeaderSize, chunk.getSecurityHeaderSize(), "securityHeaderSize mismatch"); + assertEquals(cipherTextBlockSize, chunk.getCipherTextBlockSize(), "cipherTextBlockSize mismatch"); + assertEquals(asymmetric, chunk.isAsymmetric(), "asymmetric mismatch"); + assertEquals(encrypted, chunk.isEncrypted(), "encrypted mismatch"); + assertEquals(signed, chunk.isSigned(), "signed mismatch"); + assertEquals(plainTextBlockSize, chunk.getPlainTextBlockSize(), "plainTextBlockSize mismatch"); + assertEquals(signatureSize, chunk.getSignatureSize(), "signatureSize mismatch"); + assertEquals(maxChunkSize, chunk.getMaxChunkSize(), "maxChunkSize mismatch"); + assertEquals(paddingOverhead, chunk.getPaddingOverhead(), "paddingOverhead mismatch"); + assertEquals(maxCipherTextSize, chunk.getMaxCipherTextSize(), "maxCipherTextSize mismatch"); + assertEquals(maxCipherTextBlocks, chunk.getMaxCipherTextBlocks(), "maxCipherTextBlocks mismatch"); + assertEquals(maxPlainTextSize, chunk.getMaxPlainTextSize(), "maxPlainTextSize mismatch"); + assertEquals(maxBodySize, chunk.getMaxBodySize(), "maxBodySize mismatch"); + } + + private static Entry get(int keySize) { + return CERTIFICATES.computeIfAbsent(keySize, (ks) -> TestCertificateGenerator.generate(ks, "cn=test", 10)); + } + +} \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverterTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverterTest.java new file mode 100644 index 00000000000..b7a2595dd70 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/chunk/PayloadConverterTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.plc4x.java.opcua.protocol.chunk; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import org.apache.plc4x.java.opcua.readwrite.BinaryPayload; +import org.apache.plc4x.java.opcua.readwrite.ExpandedNodeId; +import org.apache.plc4x.java.opcua.readwrite.ExtensiblePayload; +import org.apache.plc4x.java.opcua.readwrite.ExtensionObject; +import org.apache.plc4x.java.opcua.readwrite.HistoryEvent; +import org.apache.plc4x.java.opcua.readwrite.NodeIdFourByte; +import org.apache.plc4x.java.opcua.readwrite.SequenceHeader; +import org.apache.plc4x.java.spi.utils.hex.Hex; +import org.junit.jupiter.api.Test; + +class PayloadConverterTest { + + @Test + void convert() throws Exception { + ExpandedNodeId expandedNodeId = new ExpandedNodeId( + false, //Namespace Uri Specified + false, //Server Index Specified + new NodeIdFourByte( + (short) 0, 661 + ), + null, + null + ); + + ExtensionObject extObject = new ExtensionObject( + expandedNodeId, + null, + new HistoryEvent(0, Collections.emptyList()) + ); + + ExtensiblePayload payload = new ExtensiblePayload( + new SequenceHeader(1, 2), + extObject + ); + + BinaryPayload binary = PayloadConverter.toBinary(payload); + ExtensiblePayload extensible = PayloadConverter.toExtensible(binary); + + String extensibleSrcHex = Hex.dump(PayloadConverter.toStream(payload)); + String binaryDstHex = Hex.dump(PayloadConverter.toStream(binary)); + String extensibleDstHex = Hex.dump(PayloadConverter.toStream(extensible)); + + assertEquals(extensibleSrcHex, binaryDstHex); + assertEquals(extensibleSrcHex, extensibleDstHex); + } + +} \ No newline at end of file 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 7436b378d1f..58e77fa7237 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 @@ -43,6 +43,7 @@ import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaRuntimeException; +import org.eclipse.milo.opcua.stack.core.channel.EncodingLimits; import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager; import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; @@ -133,7 +134,9 @@ public TestMiloServer() throws Exception { Set endpointConfigurations = createEndpointConfigurations(certificate); + EncodingLimits limits = new EncodingLimits(8196, 64, 2097152, 128); OpcUaServerConfig serverConfig = OpcUaServerConfig.builder() + .setEncodingLimits(limits) .setApplicationUri(applicationUri) .setApplicationName(LocalizedText.english("Eclipse Milo OPC UA Example Server")) .setEndpoints(endpointConfigurations) @@ -182,23 +185,64 @@ private Set createEndpointConfigurations(X509Certificate USER_TOKEN_POLICY_X509 ); - EndpointConfiguration.Builder noSecurityBuilder = builder.copy() .setSecurityPolicy(SecurityPolicy.None) .setSecurityMode(MessageSecurityMode.None); - endpointConfigurations.add(buildTcpEndpoint(noSecurityBuilder)); // TCP Basic256Sha256 / SignAndEncrypt endpointConfigurations.add(buildTcpEndpoint( builder.copy() .setSecurityPolicy(SecurityPolicy.Basic256Sha256) + .setSecurityMode(MessageSecurityMode.Sign)) + ); + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Basic256Sha256) + .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) + ); + // TCP Basic256 / SignAndEncrypt + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Basic256) + .setSecurityMode(MessageSecurityMode.Sign)) + ); + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Basic256) .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) ); // TCP Basic128Rsa15 / SignAndEncrypt endpointConfigurations.add(buildTcpEndpoint( builder.copy() .setSecurityPolicy(SecurityPolicy.Basic128Rsa15) + .setSecurityMode(MessageSecurityMode.Sign)) + ); + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Basic128Rsa15) + .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) + ); + // TCP Aes128_Sha256_RsaOaep / SignAndEncrypt + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Aes128_Sha256_RsaOaep) + .setSecurityMode(MessageSecurityMode.Sign)) + ); + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Aes128_Sha256_RsaOaep) + .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) + ); + // TCP Aes256_Sha256_RsaPss / SignAndEncrypt + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Aes256_Sha256_RsaPss) + .setSecurityMode(MessageSecurityMode.Sign)) + ); + endpointConfigurations.add(buildTcpEndpoint( + builder.copy() + .setSecurityPolicy(SecurityPolicy.Aes256_Sha256_RsaPss) .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) ); diff --git a/plc4j/drivers/opcua/src/test/resources/chunk-calculation-1024.csv b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-1024.csv new file mode 100644 index 00000000000..0427226c0e0 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-1024.csv @@ -0,0 +1,49 @@ +keySize,securityPolicy,messageSecurity,asymmetric,encrypted,signed,securityHeaderSize,cipherTextBlockSize,plainTextBlockSize,signatureSize,maxChunkSize,paddingOverhead,maxCipherTextSize,maxCipherTextBlocks,maxPlainTextSize,maxBodySize +1024, NONE, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +1024, NONE, messageSecurityModeInvalid, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +1024, Basic128Rsa15, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic128Rsa15, messageSecurityModeInvalid, true, true, true, 501, 128, 117, 128, 8196, 1, 7683, 60, 7020, 6883 +1024, Basic256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic256, messageSecurityModeInvalid, true, true, true, 496, 128, 86, 128, 8196, 1, 7688, 60, 5160, 5023 +1024, Basic256Sha256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Basic256Sha256, messageSecurityModeInvalid, true, true, true, 502, 128, 86, 128, 8196, 1, 7682, 60, 5160, 5023 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, true, true, true, 509, 128, 86, 128, 8196, 1, 7675, 59, 5074, 4937 +1024, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, true, true, true, 508, 128, 62, 128, 8196, 1, 7676, 59, 3658, 3521 +1024, NONE, messageSecurityModeNone, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +1024, NONE, messageSecurityModeNone, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +1024, Basic128Rsa15, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic128Rsa15, messageSecurityModeNone, true, true, true, 501, 128, 117, 128, 8196, 1, 7683, 60, 7020, 6883 +1024, Basic256, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic256, messageSecurityModeNone, true, true, true, 496, 128, 86, 128, 8196, 1, 7688, 60, 5160, 5023 +1024, Basic256Sha256, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Basic256Sha256, messageSecurityModeNone, true, true, true, 502, 128, 86, 128, 8196, 1, 7682, 60, 5160, 5023 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeNone, true, true, true, 509, 128, 86, 128, 8196, 1, 7675, 59, 5074, 4937 +1024, Aes256_Sha256_RsaPss, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes256_Sha256_RsaPss, messageSecurityModeNone, true, true, true, 508, 128, 62, 128, 8196, 1, 7676, 59, 3658, 3521 +1024, NONE, messageSecurityModeSign, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +1024, NONE, messageSecurityModeSign, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +1024, Basic128Rsa15, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic128Rsa15, messageSecurityModeSign, true, true, true, 501, 128, 117, 128, 8196, 1, 7683, 60, 7020, 6883 +1024, Basic256, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +1024, Basic256, messageSecurityModeSign, true, true, true, 496, 128, 86, 128, 8196, 1, 7688, 60, 5160, 5023 +1024, Basic256Sha256, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Basic256Sha256, messageSecurityModeSign, true, true, true, 502, 128, 86, 128, 8196, 1, 7682, 60, 5160, 5023 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeSign, true, true, true, 509, 128, 86, 128, 8196, 1, 7675, 59, 5074, 4937 +1024, Aes256_Sha256_RsaPss, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +1024, Aes256_Sha256_RsaPss, messageSecurityModeSign, true, true, true, 508, 128, 62, 128, 8196, 1, 7676, 59, 3658, 3521 +1024, NONE, messageSecurityModeSignAndEncrypt, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +1024, NONE, messageSecurityModeSignAndEncrypt, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +1024, Basic128Rsa15, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +1024, Basic128Rsa15, messageSecurityModeSignAndEncrypt, true, true, true, 501, 128, 117, 128, 8196, 1, 7683, 60, 7020, 6883 +1024, Basic256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +1024, Basic256, messageSecurityModeSignAndEncrypt, true, true, true, 496, 128, 86, 128, 8196, 1, 7688, 60, 5160, 5023 +1024, Basic256Sha256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +1024, Basic256Sha256, messageSecurityModeSignAndEncrypt, true, true, true, 502, 128, 86, 128, 8196, 1, 7682, 60, 5160, 5023 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +1024, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, true, true, true, 509, 128, 86, 128, 8196, 1, 7675, 59, 5074, 4937 +1024, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +1024, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, true, true, true, 508, 128, 62, 128, 8196, 1, 7676, 59, 3658, 3521 \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/resources/chunk-calculation-2048.csv b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-2048.csv new file mode 100644 index 00000000000..683d7ce22f4 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-2048.csv @@ -0,0 +1,49 @@ +keySize,securityPolicy,messageSecurity,asymmetric,encrypted,signed,securityHeaderSize,cipherTextBlockSize,plainTextBlockSize,signatureSize,maxChunkSize,paddingOverhead,maxCipherTextSize,maxCipherTextBlocks,maxPlainTextSize,maxBodySize +2048 , NONE, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +2048 , NONE, messageSecurityModeInvalid, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +2048 , Basic128Rsa15, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic128Rsa15, messageSecurityModeInvalid, true, true, true, 762, 256, 245, 256, 8196, 1, 7422, 28, 6860, 6595 +2048 , Basic256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic256, messageSecurityModeInvalid, true, true, true, 757, 256, 214, 256, 8196, 1, 7427, 29, 6206, 5941 +2048 , Basic256Sha256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Basic256Sha256, messageSecurityModeInvalid, true, true, true, 763, 256, 214, 256, 8196, 1, 7421, 28, 5992, 5727 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, true, true, true, 770, 256, 214, 256, 8196, 1, 7414, 28, 5992, 5727 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeInvalid, true, true, true, 769, 256, 190, 256, 8196, 1, 7415, 28, 5320, 5055 +2048 , NONE, messageSecurityModeNone, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +2048 , NONE, messageSecurityModeNone, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +2048 , Basic128Rsa15, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic128Rsa15, messageSecurityModeNone, true, true, true, 762, 256, 245, 256, 8196, 1, 7422, 28, 6860, 6595 +2048 , Basic256, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic256, messageSecurityModeNone, true, true, true, 757, 256, 214, 256, 8196, 1, 7427, 29, 6206, 5941 +2048 , Basic256Sha256, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Basic256Sha256, messageSecurityModeNone, true, true, true, 763, 256, 214, 256, 8196, 1, 7421, 28, 5992, 5727 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeNone, true, true, true, 770, 256, 214, 256, 8196, 1, 7414, 28, 5992, 5727 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeNone, true, true, true, 769, 256, 190, 256, 8196, 1, 7415, 28, 5320, 5055 +2048 , NONE, messageSecurityModeSign, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +2048 , NONE, messageSecurityModeSign, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +2048 , Basic128Rsa15, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic128Rsa15, messageSecurityModeSign, true, true, true, 762, 256, 245, 256, 8196, 1, 7422, 28, 6860, 6595 +2048 , Basic256, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +2048 , Basic256, messageSecurityModeSign, true, true, true, 757, 256, 214, 256, 8196, 1, 7427, 29, 6206, 5941 +2048 , Basic256Sha256, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Basic256Sha256, messageSecurityModeSign, true, true, true, 763, 256, 214, 256, 8196, 1, 7421, 28, 5992, 5727 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeSign, true, true, true, 770, 256, 214, 256, 8196, 1, 7414, 28, 5992, 5727 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeSign, true, true, true, 769, 256, 190, 256, 8196, 1, 7415, 28, 5320, 5055 +2048 , NONE, messageSecurityModeSignAndEncrypt, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +2048 , NONE, messageSecurityModeSignAndEncrypt, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +2048 , Basic128Rsa15, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +2048 , Basic128Rsa15, messageSecurityModeSignAndEncrypt, true, true, true, 762, 256, 245, 256, 8196, 1, 7422, 28, 6860, 6595 +2048 , Basic256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +2048 , Basic256, messageSecurityModeSignAndEncrypt, true, true, true, 757, 256, 214, 256, 8196, 1, 7427, 29, 6206, 5941 +2048 , Basic256Sha256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +2048 , Basic256Sha256, messageSecurityModeSignAndEncrypt, true, true, true, 763, 256, 214, 256, 8196, 1, 7421, 28, 5992, 5727 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +2048 , Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, true, true, true, 770, 256, 214, 256, 8196, 1, 7414, 28, 5992, 5727 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +2048 , Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, true, true, true, 769, 256, 190, 256, 8196, 1, 7415, 28, 5320, 5055 \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/resources/chunk-calculation-3072.csv b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-3072.csv new file mode 100644 index 00000000000..8cf2e69529e --- /dev/null +++ b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-3072.csv @@ -0,0 +1,49 @@ +keySize,securityPolicy,messageSecurity,asymmetric,encrypted,signed,securityHeaderSize,cipherTextBlockSize,plainTextBlockSize,signatureSize,maxChunkSize,paddingOverhead,maxCipherTextSize,maxCipherTextBlocks,maxPlainTextSize,maxBodySize +3072, NONE, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +3072, NONE, messageSecurityModeInvalid, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +3072, Basic128Rsa15, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic128Rsa15, messageSecurityModeInvalid, true, true, true, 1018, 384, 373, 384, 8196, 2, 7166, 18, 6714, 6320 +3072, Basic256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic256, messageSecurityModeInvalid, true, true, true, 1013, 384, 342, 384, 8196, 2, 7171, 18, 6156, 5762 +3072, Basic256Sha256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Basic256Sha256, messageSecurityModeInvalid, true, true, true, 1019, 384, 342, 384, 8196, 2, 7165, 18, 6156, 5762 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, true, true, true, 1026, 384, 342, 384, 8196, 2, 7158, 18, 6156, 5762 +3072, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, true, true, true, 1025, 384, 318, 384, 8196, 2, 7159, 18, 5724, 5330 +3072, NONE, messageSecurityModeNone, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +3072, NONE, messageSecurityModeNone, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +3072, Basic128Rsa15, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic128Rsa15, messageSecurityModeNone, true, true, true, 1018, 384, 373, 384, 8196, 2, 7166, 18, 6714, 6320 +3072, Basic256, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic256, messageSecurityModeNone, true, true, true, 1013, 384, 342, 384, 8196, 2, 7171, 18, 6156, 5762 +3072, Basic256Sha256, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Basic256Sha256, messageSecurityModeNone, true, true, true, 1019, 384, 342, 384, 8196, 2, 7165, 18, 6156, 5762 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeNone, true, true, true, 1026, 384, 342, 384, 8196, 2, 7158, 18, 6156, 5762 +3072, Aes256_Sha256_RsaPss, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes256_Sha256_RsaPss, messageSecurityModeNone, true, true, true, 1025, 384, 318, 384, 8196, 2, 7159, 18, 5724, 5330 +3072, NONE, messageSecurityModeSign, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +3072, NONE, messageSecurityModeSign, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +3072, Basic128Rsa15, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic128Rsa15, messageSecurityModeSign, true, true, true, 1018, 384, 373, 384, 8196, 2, 7166, 18, 6714, 6320 +3072, Basic256, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +3072, Basic256, messageSecurityModeSign, true, true, true, 1013, 384, 342, 384, 8196, 2, 7171, 18, 6156, 5762 +3072, Basic256Sha256, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Basic256Sha256, messageSecurityModeSign, true, true, true, 1019, 384, 342, 384, 8196, 2, 7165, 18, 6156, 5762 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeSign, true, true, true, 1026, 384, 342, 384, 8196, 2, 7158, 18, 6156, 5762 +3072, Aes256_Sha256_RsaPss, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +3072, Aes256_Sha256_RsaPss, messageSecurityModeSign, true, true, true, 1025, 384, 318, 384, 8196, 2, 7159, 18, 5724, 5330 +3072, NONE, messageSecurityModeSignAndEncrypt, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +3072, NONE, messageSecurityModeSignAndEncrypt, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +3072, Basic128Rsa15, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +3072, Basic128Rsa15, messageSecurityModeSignAndEncrypt, true, true, true, 1018, 384, 373, 384, 8196, 2, 7166, 18, 6714, 6320 +3072, Basic256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +3072, Basic256, messageSecurityModeSignAndEncrypt, true, true, true, 1013, 384, 342, 384, 8196, 2, 7171, 18, 6156, 5762 +3072, Basic256Sha256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +3072, Basic256Sha256, messageSecurityModeSignAndEncrypt, true, true, true, 1019, 384, 342, 384, 8196, 2, 7165, 18, 6156, 5762 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +3072, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, true, true, true, 1026, 384, 342, 384, 8196, 2, 7158, 18, 6156, 5762 +3072, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +3072, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, true, true, true, 1025, 384, 318, 384, 8196, 2, 7159, 18, 5724, 5330 \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/resources/chunk-calculation-4096.csv b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-4096.csv new file mode 100644 index 00000000000..772b10e4478 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-4096.csv @@ -0,0 +1,49 @@ +keySize,securityPolicy,messageSecurity,asymmetric,encrypted,signed,securityHeaderSize,cipherTextBlockSize,plainTextBlockSize,signatureSize,maxChunkSize,paddingOverhead,maxCipherTextSize,maxCipherTextBlocks,maxPlainTextSize,maxBodySize +4096, NONE, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +4096, NONE, messageSecurityModeInvalid, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +4096, Basic128Rsa15, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic128Rsa15, messageSecurityModeInvalid, true, true, true, 1274, 512, 501, 512, 8196, 2, 6910, 13, 6513, 5991 +4096, Basic256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic256, messageSecurityModeInvalid, true, true, true, 1269, 512, 470, 512, 8196, 2, 6915, 13, 6110, 5588 +4096, Basic256Sha256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Basic256Sha256, messageSecurityModeInvalid, true, true, true, 1275, 512, 470, 512, 8196, 2, 6909, 13, 6110, 5588 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, true, true, true, 1282, 512, 470, 512, 8196, 2, 6902, 13, 6110, 5588 +4096, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, true, true, true, 1281, 512, 446, 512, 8196, 2, 6903, 13, 5798, 5276 +4096, NONE, messageSecurityModeNone, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +4096, NONE, messageSecurityModeNone, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +4096, Basic128Rsa15, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic128Rsa15, messageSecurityModeNone, true, true, true, 1274, 512, 501, 512, 8196, 2, 6910, 13, 6513, 5991 +4096, Basic256, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic256, messageSecurityModeNone, true, true, true, 1269, 512, 470, 512, 8196, 2, 6915, 13, 6110, 5588 +4096, Basic256Sha256, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Basic256Sha256, messageSecurityModeNone, true, true, true, 1275, 512, 470, 512, 8196, 2, 6909, 13, 6110, 5588 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeNone, true, true, true, 1282, 512, 470, 512, 8196, 2, 6902, 13, 6110, 5588 +4096, Aes256_Sha256_RsaPss, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes256_Sha256_RsaPss, messageSecurityModeNone, true, true, true, 1281, 512, 446, 512, 8196, 2, 6903, 13, 5798, 5276 +4096, NONE, messageSecurityModeSign, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +4096, NONE, messageSecurityModeSign, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +4096, Basic128Rsa15, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic128Rsa15, messageSecurityModeSign, true, true, true, 1274, 512, 501, 512, 8196, 2, 6910, 13, 6513, 5991 +4096, Basic256, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +4096, Basic256, messageSecurityModeSign, true, true, true, 1269, 512, 470, 512, 8196, 2, 6915, 13, 6110, 5588 +4096, Basic256Sha256, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Basic256Sha256, messageSecurityModeSign, true, true, true, 1275, 512, 470, 512, 8196, 2, 6909, 13, 6110, 5588 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeSign, true, true, true, 1282, 512, 470, 512, 8196, 2, 6902, 13, 6110, 5588 +4096, Aes256_Sha256_RsaPss, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +4096, Aes256_Sha256_RsaPss, messageSecurityModeSign, true, true, true, 1281, 512, 446, 512, 8196, 2, 6903, 13, 5798, 5276 +4096, NONE, messageSecurityModeSignAndEncrypt, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +4096, NONE, messageSecurityModeSignAndEncrypt, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +4096, Basic128Rsa15, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +4096, Basic128Rsa15, messageSecurityModeSignAndEncrypt, true, true, true, 1274, 512, 501, 512, 8196, 2, 6910, 13, 6513, 5991 +4096, Basic256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +4096, Basic256, messageSecurityModeSignAndEncrypt, true, true, true, 1269, 512, 470, 512, 8196, 2, 6915, 13, 6110, 5588 +4096, Basic256Sha256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +4096, Basic256Sha256, messageSecurityModeSignAndEncrypt, true, true, true, 1275, 512, 470, 512, 8196, 2, 6909, 13, 6110, 5588 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +4096, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, true, true, true, 1282, 512, 470, 512, 8196, 2, 6902, 13, 6110, 5588 +4096, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +4096, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, true, true, true, 1281, 512, 446, 512, 8196, 2, 6903, 13, 5798, 5276 \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/test/resources/chunk-calculation-5120.csv b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-5120.csv new file mode 100644 index 00000000000..50e657cda78 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/resources/chunk-calculation-5120.csv @@ -0,0 +1,49 @@ +keySize,securityPolicy,messageSecurity,asymmetric,encrypted,signed,securityHeaderSize,cipherTextBlockSize,plainTextBlockSize,signatureSize,maxChunkSize,paddingOverhead,maxCipherTextSize,maxCipherTextBlocks,maxPlainTextSize,maxBodySize +5120, NONE, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +5120, NONE, messageSecurityModeInvalid, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +5120, Basic128Rsa15, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic128Rsa15, messageSecurityModeInvalid, true, true, true, 1530, 640, 629, 640, 8196, 2, 6654, 10, 6290, 5640 +5120, Basic256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic256, messageSecurityModeInvalid, true, true, true, 1525, 640, 598, 640, 8196, 2, 6659, 10, 5980, 5330 +5120, Basic256Sha256, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Basic256Sha256, messageSecurityModeInvalid, true, true, true, 1531, 640, 598, 640, 8196, 2, 6653, 10, 5980, 5330 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeInvalid, true, true, true, 1538, 640, 598, 640, 8196, 2, 6646, 10, 5980, 5330 +5120, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes256_Sha256_RsaPss, messageSecurityModeInvalid, true, true, true, 1537, 640, 574, 640, 8196, 2, 6647, 10, 5740, 5090 +5120, NONE, messageSecurityModeNone, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +5120, NONE, messageSecurityModeNone, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +5120, Basic128Rsa15, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic128Rsa15, messageSecurityModeNone, true, true, true, 1530, 640, 629, 640, 8196, 2, 6654, 10, 6290, 5640 +5120, Basic256, messageSecurityModeNone, false, false, false, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic256, messageSecurityModeNone, true, true, true, 1525, 640, 598, 640, 8196, 2, 6659, 10, 5980, 5330 +5120, Basic256Sha256, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Basic256Sha256, messageSecurityModeNone, true, true, true, 1531, 640, 598, 640, 8196, 2, 6653, 10, 5980, 5330 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeNone, true, true, true, 1538, 640, 598, 640, 8196, 2, 6646, 10, 5980, 5330 +5120, Aes256_Sha256_RsaPss, messageSecurityModeNone, false, false, false, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes256_Sha256_RsaPss, messageSecurityModeNone, true, true, true, 1537, 640, 574, 640, 8196, 2, 6647, 10, 5740, 5090 +5120, NONE, messageSecurityModeSign, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +5120, NONE, messageSecurityModeSign, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +5120, Basic128Rsa15, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic128Rsa15, messageSecurityModeSign, true, true, true, 1530, 640, 629, 640, 8196, 2, 6654, 10, 6290, 5640 +5120, Basic256, messageSecurityModeSign, false, false, true, 4, 1, 1, 20, 8196, 0, 8180, 8180, 8180, 8152 +5120, Basic256, messageSecurityModeSign, true, true, true, 1525, 640, 598, 640, 8196, 2, 6659, 10, 5980, 5330 +5120, Basic256Sha256, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Basic256Sha256, messageSecurityModeSign, true, true, true, 1531, 640, 598, 640, 8196, 2, 6653, 10, 5980, 5330 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeSign, true, true, true, 1538, 640, 598, 640, 8196, 2, 6646, 10, 5980, 5330 +5120, Aes256_Sha256_RsaPss, messageSecurityModeSign, false, false, true, 4, 1, 1, 32, 8196, 0, 8180, 8180, 8180, 8140 +5120, Aes256_Sha256_RsaPss, messageSecurityModeSign, true, true, true, 1537, 640, 574, 640, 8196, 2, 6647, 10, 5740, 5090 +5120, NONE, messageSecurityModeSignAndEncrypt, false, false, false, 4, 1, 1, 0, 8196, 0, 8180, 8180, 8180, 8172 +5120, NONE, messageSecurityModeSignAndEncrypt, true, false, false, 59, 1, 1, 0, 8196, 0, 8125, 8125, 8125, 8117 +5120, Basic128Rsa15, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +5120, Basic128Rsa15, messageSecurityModeSignAndEncrypt, true, true, true, 1530, 640, 629, 640, 8196, 2, 6654, 10, 6290, 5640 +5120, Basic256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 20, 8196, 1, 8180, 511, 8176, 8147 +5120, Basic256, messageSecurityModeSignAndEncrypt, true, true, true, 1525, 640, 598, 640, 8196, 2, 6659, 10, 5980, 5330 +5120, Basic256Sha256, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +5120, Basic256Sha256, messageSecurityModeSignAndEncrypt, true, true, true, 1531, 640, 598, 640, 8196, 2, 6653, 10, 5980, 5330 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +5120, Aes128_Sha256_RsaOaep, messageSecurityModeSignAndEncrypt, true, true, true, 1538, 640, 598, 640, 8196, 2, 6646, 10, 5980, 5330 +5120, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, false, true, true, 4, 16, 16, 32, 8196, 1, 8180, 511, 8176, 8135 +5120, Aes256_Sha256_RsaPss, messageSecurityModeSignAndEncrypt, true, true, true, 1537, 640, 574, 640, 8196, 2, 6647, 10, 5740, 5090 \ No newline at end of file