diff --git a/plc4j/drivers/opcua/pom.xml b/plc4j/drivers/opcua/pom.xml index 7a5c6b44a44..dab62b94f57 100644 --- a/plc4j/drivers/opcua/pom.xml +++ b/plc4j/drivers/opcua/pom.xml @@ -98,6 +98,26 @@ + + + org.apache.rat + apache-rat-plugin + + + license-check + verify + + check + + + + + + + src/test/resources/chunk-calculation*.csv + + + @@ -127,6 +147,10 @@ org.apache.commons commons-lang3 + + commons-codec + commons-codec + io.vavr 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..4fdaee87967 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/Limits.java @@ -0,0 +1,77 @@ +/* + * 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_RECEIVE_BUFFER_SIZE = 65535; + private static final int DEFAULT_SEND_BUFFER_SIZE = 65535; + private static final int DEFAULT_MAX_MESSAGE_SIZE = 2097152; + private static final int DEFAULT_MAX_CHUNK_COUNT = 64; + + @ConfigurationParameter("receiveBufferSize") + private int receiveBufferSize; + @ConfigurationParameter("sendBufferSize") + private int sendBufferSize; + @ConfigurationParameter("maxMessageSize") + private int maxMessageSize; + @ConfigurationParameter("maxChunkCount") + private int maxChunkCount; + + public Limits() { + this(DEFAULT_RECEIVE_BUFFER_SIZE, DEFAULT_SEND_BUFFER_SIZE, DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_MAX_CHUNK_COUNT); + } + + 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..d3357289820 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,29 @@ */ package org.apache.plc4x.java.opcua.config; -import org.apache.plc4x.java.opcua.readwrite.PascalByteString; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import org.apache.plc4x.java.opcua.context.SecureChannel; +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_CHANNEL_LIFETIME = 3600000; + + public static final long DEFAULT_SESSION_TIMEOUT = 120000; + public static final long DEFAULT_NEGOTIATION_TIMEOUT = 60000; + + public static final long DEFAULT_REQUEST_TIMEOUT = 30000; + @ConfigurationParameter("protocolCode") private String protocolCode; @@ -49,16 +63,47 @@ 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; - @ConfigurationParameter("certDirectory") - private String certDirectory; + @ConfigurationParameter("keyStoreType") + private String keyStoreType = KeyStore.getDefaultType(); @ConfigurationParameter("keyStorePassword") private String keyStorePassword; - private byte[] senderCertificate; - private PascalByteString thumbprint; + + @ConfigurationParameter("serverCertificateFile") + private String serverCertificateFile; + + @ConfigurationParameter("trustStoreFile") + private String trustStoreFile; + + @ConfigurationParameter("trustStoreType") + private String trustStoreType = KeyStore.getDefaultType(); + + @ConfigurationParameter("trustStorePassword") + private String trustStorePassword; + + // the discovered certificate when discovery is enabled + private X509Certificate serverCertificate; + + @ConfigurationParameter("channelLifetime") + private long channelLifetime = DEFAULT_CHANNEL_LIFETIME; + + @ConfigurationParameter("sessionTimeout") + private long sessionTimeout = DEFAULT_SESSION_TIMEOUT; + + @ConfigurationParameter("negotiationTimeout") + private long negotiationTimeout = DEFAULT_NEGOTIATION_TIMEOUT; + + @ConfigurationParameter("requestTimeout") + private long requestTimeout = DEFAULT_REQUEST_TIMEOUT; + + @ComplexConfigurationParameter(prefix = "encoding", defaultOverrides = {}, requiredOverrides = {}) + private Limits limits = new Limits(); public String getProtocolCode() { return protocolCode; @@ -84,20 +129,73 @@ public String getPassword() { return password; } - public String getCertDirectory() { - return certDirectory; - } - public SecurityPolicy getSecurityPolicy() { return securityPolicy; } + public MessageSecurity getMessageSecurity() { + return messageSecurity; + } + public String getKeyStoreFile() { return keyStoreFile; } - public String getKeyStorePassword() { - return keyStorePassword; + public String getKeyStoreType() { + return keyStoreType; + } + + public char[] getKeyStorePassword() { + return keyStorePassword == null ? null : keyStorePassword.toCharArray(); + } + + public String getTrustStoreFile() { + return trustStoreFile; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public char[] getTrustStorePassword() { + return trustStorePassword == null ? null : trustStorePassword.toCharArray(); + } + + public Limits getEncodingLimits() { + return limits; + } + + public X509Certificate getServerCertificate() { + if (serverCertificate == null && serverCertificateFile != null) { + // initialize server certificate from configured file + try { + byte[] certificateBytes = Files.readAllBytes(Path.of(serverCertificateFile)); + serverCertificate = SecureChannel.getX509Certificate(certificateBytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return serverCertificate; + } + + public void setServerCertificate(X509Certificate serverCertificate) { + this.serverCertificate = serverCertificate; + } + + public long getChannelLifetime() { + return channelLifetime; + } + + public long getSessionTimeout() { + return sessionTimeout; + } + + public long getRequestTimeout() { + return requestTimeout; + } + + public long getNegotiationTimeout() { + return negotiationTimeout; } @Override @@ -108,26 +206,9 @@ public String toString() { ", password='" + (password != null ? "******" : null) + '\'' + ", securityPolicy='" + securityPolicy + '\'' + ", 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..29caf9fd557 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,8 @@ package org.apache.plc4x.java.opcua.context; import io.vavr.control.Try; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; import org.bouncycastle.asn1.x509.GeneralName; import java.security.KeyPair; @@ -34,7 +36,7 @@ public class CertificateKeyPair { private final X509Certificate certificate; private final byte[] thumbprint; - public CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) throws Exception { + public CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) throws GeneralSecurityException { this.keyPair = keyPair; this.certificate = certificate; MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); @@ -49,6 +51,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..d398d76951e --- /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.getNegotiationTimeout()) + .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.getNegotiationTimeout()) + .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..fd8862fc42f 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 @@ -19,11 +19,19 @@ package org.apache.plc4x.java.opcua.context; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; 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; +import org.apache.plc4x.java.opcua.security.CertificateVerifier; +import org.apache.plc4x.java.opcua.security.PermissiveCertificateVerifier; import org.apache.plc4x.java.opcua.security.SecurityPolicy; +import org.apache.plc4x.java.opcua.security.TrustStoreCertificateVerifier; import org.apache.plc4x.java.spi.configuration.HasConfiguration; import org.apache.plc4x.java.spi.context.DriverContext; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -32,8 +40,6 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.nio.file.FileSystems; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; @@ -67,37 +73,38 @@ public class OpcuaDriverContext implements DriverContext, HasConfiguration getApplicationUri() { .flatMap(CertificateKeyPair::getApplicationUri); } + public PascalByteString getThumbprint() { + return thumbprint; + } + + public CertificateVerifier getCertificateVerifier() { + return certificateVerifier; + } + + private static KeyStore openKeyStore(String keyStoreFile, String keyStoreType, char[] password) throws IOException, GeneralSecurityException { + File serverKeyStore = null; + if (keyStoreFile != null) { + serverKeyStore = Paths.get(keyStoreFile).toFile(); + } + if (keyStoreFile == null || !serverKeyStore.exists()) { + throw new FileNotFoundException("Invalid parameter - specified file " + keyStoreFile + " does not exist"); + } + + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(new FileInputStream(serverKeyStore), password); + return keyStore; + } + } 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..54494139073 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,9 @@ 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,33 +61,10 @@ 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 Pattern INET_ADDRESS_PATTERN = Pattern.compile("(.(?tcp))?://" + + 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|https?))?://" + "(?[\\w.-]+)(:" + "(?\\d*))?"); @@ -99,45 +73,34 @@ 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 final 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()); + this.sessionTimeout = configuration.getSessionTimeout(); if (authentication != null) { if (authentication instanceof PlcUsernamePasswordAuthentication) { this.username = ((PlcUsernamePasswordAuthentication) authentication).getUsername(); @@ -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.getNegotiationTimeout(), 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,80 @@ 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(); + // make sure returned certificate is trusted + driverContext.getCertificateVerifier().checkCertificateTrusted(remoteCertificate); + + 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 +332,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 +353,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 +423,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); } /** @@ -1250,10 +528,11 @@ private void selectEndpoint(CreateSessionResponse sessionResponse) throws PlcRun */ private boolean isEndpoint(EndpointDescription endpoint) throws PlcRuntimeException { // Split up the connection string into it's individual segments. - Matcher matcher = URI_PATTERN.matcher(endpoint.getEndpointUrl().getStringValue()); + String endpointUri = endpoint.getEndpointUrl().getStringValue(); + Matcher matcher = URI_PATTERN.matcher(endpointUri); if (!matcher.matches()) { throw new PlcRuntimeException( - "Endpoint returned from the server doesn't match the format '{protocol-code}:({transport-code})?//{transport-host}(:{transport-port})(/{transport-endpoint})'"); + "Endpoint " + endpointUri + " returned from the server doesn't match the format '{protocol-code}:({transport-code})?//{transport-host}(:{transport-port})(/{transport-endpoint})'"); } LOGGER.trace("Using Endpoint {} {} {}", matcher.group("transportHost"), matcher.group("transportPort"), matcher.group("transportEndpoint")); @@ -1325,17 +604,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 +636,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..f3286daaff3 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,19 +32,22 @@ 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.*; +import org.apache.plc4x.java.opcua.security.SecurityPolicy; import org.apache.plc4x.java.opcua.tag.OpcuaTag; import org.apache.plc4x.java.spi.ConversationContext; 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 +60,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 +79,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 +105,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 +120,77 @@ 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 (!configuration.isDiscovery()) { + 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) { + PlcConnectionException exception = new PlcConnectionException(error); + context.getChannel().pipeline().fireExceptionCaught(exception); + transaction.failRequest(exception); + return; + } + configuration.setServerCertificate(getX509Certificate(response.getServerCertificate().getStringValue())); + context.fireDiscovered(configuration); + context.fireDisconnected(); + transaction.endRequest(); + }); } - 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 +213,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 +244,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 +398,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 +413,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 +703,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 +730,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 +746,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 +756,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 +807,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 +850,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..1d514703fe5 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java @@ -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; @@ -41,51 +45,43 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.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 +125,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 +134,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 +151,69 @@ 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); + // do not send -1 when requesting publish, the -1 value indicates NULL value + // which might result in corruption of subscription for some servers + int ackLength = 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 +223,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 +279,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/CertificateVerifier.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/CertificateVerifier.java new file mode 100644 index 00000000000..f943c569bae --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/CertificateVerifier.java @@ -0,0 +1,29 @@ +/* + * 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 java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public interface CertificateVerifier { + + void checkCertificateTrusted(X509Certificate certificate) throws CertificateException; + +} 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/PermissiveCertificateVerifier.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/PermissiveCertificateVerifier.java new file mode 100644 index 00000000000..b32789ac330 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/PermissiveCertificateVerifier.java @@ -0,0 +1,35 @@ +/* + * 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 java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * The trust manager which does trust all certificates and does not complain on anything. + */ +public class PermissiveCertificateVerifier implements CertificateVerifier { + + @Override + public void checkCertificateTrusted(X509Certificate certificate) throws CertificateException { + } + + +} \ No newline at end of file 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/main/java/org/apache/plc4x/java/opcua/security/TrustStoreCertificateVerifier.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/TrustStoreCertificateVerifier.java new file mode 100644 index 00000000000..9eee142ed27 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/security/TrustStoreCertificateVerifier.java @@ -0,0 +1,55 @@ +/* + * 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 java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public class TrustStoreCertificateVerifier implements CertificateVerifier { + + private X509TrustManager trustManager; + + public TrustStoreCertificateVerifier(KeyStore trustStore) throws GeneralSecurityException { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + TrustManager[] managers = trustManagerFactory.getTrustManagers(); + for (TrustManager manager : managers) { + if (manager instanceof X509TrustManager) { + trustManager = (X509TrustManager) manager; + break; + } + } + + if (trustManager == null) { + throw new IllegalStateException("Could not initialize trust manager, underlying trust provider not found"); + } + } + + @Override + public void checkCertificateTrusted(X509Certificate certificate) throws CertificateException { + trustManager.checkClientTrusted(new X509Certificate[]{ certificate }, "UNKNOWN"); + } + +} 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..57efdbfed09 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,13 @@ */ package org.apache.plc4x.java.opcua; -import io.vavr.collection.List; +import java.lang.reflect.Array; +import java.net.URLEncoder; +import java.nio.charset.Charset; 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 +33,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 +42,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 +62,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 +86,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 +240,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 +248,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 +265,7 @@ Stream connectionWithDiscoveryParam() throws Exception { })) .map(DynamicNode.class::cast) .iterator())) - .map(DynamicNode.class::cast) - .toJavaStream(); + .map(DynamicNode.class::cast); } @Test @@ -315,162 +324,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,29 +513,52 @@ 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: - 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(); + case Aes128_Sha256_RsaOaep: + case Aes256_Sha256_RsaPss: + Path keyStoreFile = Paths.get(System.getProperty("java.io.tmpdir"), "server", "security", "example-server.pfx"); String connectionParams = Stream.of( - Map.entry("keyStoreFile", keyStoreFile), - Map.entry("certDirectory", certDirectory), - Map.entry("keyStorePassword", "password"), - Map.entry("securityPolicy", policy) + entry("keyStoreFile", keyStoreFile.toAbsolutePath().toString().replace("\\", "/")), // handle windows paths + entry("keyStorePassword", "password"), + entry("securityPolicy", policy.name()), + entry("messageSecurity", messageSecurity.name()) ) - .map(tuple -> tuple.getKey() + "=" + tuple.getValue()) + .map(tuple -> tuple.getKey() + "=" + URLEncoder.encode(tuple.getValue(), Charset.defaultCharset())) .collect(Collectors.joining(paramDivider)); - return tcpConnectionAddress + paramSectionDivider + connectionParams; default: 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..39258661406 100644 --- a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java +++ b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java @@ -19,7 +19,6 @@ package org.eclipse.milo.examples.server; -import static com.google.common.collect.Lists.newArrayList; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME; import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_X509; @@ -43,6 +42,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 +133,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) @@ -162,7 +164,7 @@ public TestMiloServer() throws Exception { private Set createEndpointConfigurations(X509Certificate certificate) { Set endpointConfigurations = new LinkedHashSet<>(); - List bindAddresses = newArrayList(); + List bindAddresses = new ArrayList<>(); bindAddresses.add("0.0.0.0"); Set hostnames = new LinkedHashSet<>(); @@ -182,23 +184,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 diff --git a/protocols/opcua/src/test/resources/protocols/opcua/DriverTestsuite.xml b/protocols/opcua/src/test/resources/protocols/opcua/DriverTestsuite.xml index cfa23c13d9a..09dd484b99f 100644 --- a/protocols/opcua/src/test/resources/protocols/opcua/DriverTestsuite.xml +++ b/protocols/opcua/src/test/resources/protocols/opcua/DriverTestsuite.xml @@ -27,6 +27,12 @@ opcua read-write opcua + + + discovery + false + + Hello Request Response diff --git a/src/site/asciidoc/users/protocols/opc-ua.adoc b/src/site/asciidoc/users/protocols/opc-ua.adoc index f45be40c4d2..29436f45aff 100644 --- a/src/site/asciidoc/users/protocols/opc-ua.adoc +++ b/src/site/asciidoc/users/protocols/opc-ua.adoc @@ -64,8 +64,39 @@ will propagate over an '
/discovery' endpoint. The most common issue her configured and propagate the wrong external IP or URL address. If that is the case you can disable the discovery by configuring it with a `false` value. -|| `username` | A username to authenticate to the OPCUA server with. -|| `password` | A password to authenticate to the OPCUA server with. | +The discovery phase is always conducted using `NONE` security policy. + +| `securityPolicy` | `NONE` | The security policy applied to communication channel between driver and OPC UA server. +Default value assumes. Possible options are `NONE`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss`. +| `messageSecurity` | `SIGN_ENCRYPT` | The security policy applied to messages exchanged after handshake phase. +Possible options are `NONE`, `SIGN`, `SIGN_ENCRYPT`. +This option is effective only when `securityPolicy` turns encryption (anything beyond `NONE`). + +| `serverCertificateFile` | | Filesystem location where server certificate is located, supported formats are `DER` and `PEM`. +This option is required when `discovery` is disabled and `securityPolicy` is not `NONE`. +| `keyStoreFile` | | The Keystore file used to lookup client certificate and its private key. +| `keyStoreType` | `pkcs12` | Keystore type used to access keystore and private key, defaults to PKCS (for Java 11+). +Possible values are between others `jks`, `pkcs11`, `dks`, `jceks`. +| `keyStorePassword` | | Java keystore password used to access keystore and private key. +| `trustStoreFile` | | The trust store file used to verify server certificates and its chain. +| `trustStoreType` | `pkcs12` | Keystore type used to access keystore and private key, defaults to PKCS (for Java 11+). +Possible values are between others `jks`, `pkcs11`, `dks`, `jceks`. +| `trustStorePassword` | | Password used to open trust store. + +| `username` | | A username to authenticate to the OPCUA server with. +| `password` | | A password to authenticate to the OPCUA server with. + +3+| TCP encoding options +| `encoding.receiveBufferSize` | 65535 | Maximum size of received TCP transport message chunk value in bytes. +| `encoding.sendBufferSize` | 65535 | Maximum size of sent transport message chunk. +| `encoding.maxMessageSize` | 2097152 | Maximum size of complete message. +| `encoding.maxChunkCount` | 64 | Maximum number of chunks for both sent and received messages. + +3+| Timeout options +| `channelLifetime` | `3600000` | Time for which negotiated secure channel, its keys and session remains open. Value in milliseconds, by default 60 minutes. +| `sessionTimeout` | `120000` | Expiry time for opened secure session, value in milliseconds. Defaults to 2 minutes. +| `negotiationTimeout` | `60000` | Timeout for all negotiation steps prior acceptance of application level operations - this timeout applies to open secure channel, create session and close calls. Defaults to 60 seconds. +| `requestTimeout` | `30000` | Timeout for read/write/subscribe calls. Value in milliseconds. |=== @@ -89,6 +120,45 @@ opcua:tcp://127.0.0.1:12686?discovery=true&username=admin&password=password Note the transport, port and options fields are optional. +=== Secure communication +The secure channel implementation within Apache PLC4X project have been tested against existing open source server implementations. +This includes Eclipse Milo (all modes) as well as OPC Foundation .NET server (except `Basic128Rsa15`). +Manual tests proven that driver is able to communicate with OPC UA server launched on PLCs as well as commercial simulators. + +Depending on actual configuration of remote end there might be necessity to prepare client certificate. +Preparation of certificate is beyond driver, however in case when no client certificate is provided, it will be auto-generated to establish a session. + +The security modes differ between themselves by strength of applied signature and encryption algorithms. +Driver is able to communicate with single security mode at the time. +Additionally, to security policy it is possible to specify `messageSecurity` option which indicates expected security settings after initial handshake. +By default, this option is set to `SIGN_ENCRYPT` which imposes high security settings and full encryption of exchanged message payloads. +In case when additional diagnostics is needed payloads has to be traced through TRACE level log entries. +The `SIGN` mode gives possibility o browse packets in tools such wireshark. + +==== Certificate verification +The OPC UA specification defines its own procedures for certificate validation. +In order to simplify implementation by default server certificate validation is relaxed. +Unless explicitly disabled through configuration of `trustStoreFile` all server certificates will be accepted without validation. + +In case when secure communication is enabled the `trustStoreFile` option might be used to point certificates which client should accept. +The acceptance rely on regular TLS checks (expiry date, certificate path etc.), does not validate OPC UA specific parts such as application URI. + +==== Negotiation procedure +Depending on settings driver might or might not attempt to discover endpoints from remote server. +In case when `discovery` option is set to `true` driver will look up server certificate through connection attempt. +The discovery option also enables checks of server endpoints for matching security settings. + +Once initial discovery is completed and driver finds endpoint matching its security settings it will launch second connection attempt which will switch to configured security mode. + +Each connection attempt made by driver attempt to use limits described in table above. +Role of these options is declaration of values accepted and expected by client. +Once server returns its limits (`Acknowledge` for supplied `Hello` call) driver picks values from these. +The only one note is that driver takes minimum of local receive and remote send buffer size. +It does same with local send and remote receive buffer. + +Usual values of `sendBufferSize` and `receiveBufferSize` PLC devices remain at 8196 bytes. + +NOTE: Due to lack of complete implementation of negotiation and chunking logic the OPC UA driver prior Apache PLC4X 0.11 release could supply calls exceeding server limits. === Address Format To read, write and subscribe to data, the OPC UA driver uses the variable declaration string of the OPC UA server it is