From 97c135fcf3dfacb2cdebf69fde262bfcdd288dd2 Mon Sep 17 00:00:00 2001 From: Kevin Herron Date: Tue, 4 Nov 2025 09:49:15 -0800 Subject: [PATCH 1/2] Replace Bouncy Castle with JCA for security operations - Add CipherFactory utility for creating JCA-configured ciphers - Update SecurityAlgorithm to use explicit AlgorithmParameterSpec - Configure RSASSA-PSS with SHA-256 digest and MGF1, 32-byte salt - Configure RSA-OAEP with SHA-256 for hash and MGF1 - Centralize cipher initialization in CipherFactory - Update signature verification to use AlgorithmParameterSpec - Remove inline cipher creation in ChunkEncoder/ChunkDecoder - Remove inline cipher creation in UsernameProvider - Remove inline cipher creation in AbstractIdentityValidator - Remove `bcprov-jdk18on` dependency --- .../examples/client/ClientExampleRunner.java | 7 - .../milo/examples/server/ExampleServer.java | 7 +- .../milo/opcua/sdk/test/TestServer.java | 7 - .../sdk/client/identity/UsernameProvider.java | 19 +- .../identity/AbstractIdentityValidator.java | 16 +- opc-ua-stack/stack-core/pom.xml | 5 - .../stack/core/channel/ChunkDecoder.java | 24 +- .../stack/core/channel/ChunkEncoder.java | 15 +- .../core/security/SecurityAlgorithm.java | 63 +- .../opcua/stack/core/util/CipherFactory.java | 99 +++ .../util/SelfSignedCertificateGenerator.java | 7 +- .../opcua/stack/core/util/SignatureUtil.java | 25 +- .../CaSignedCertificateBuilder.java | 5 +- .../CertificateValidationUtilTest.java | 10 - .../opcua/stack/ChunkSerializationTest.java | 7 - .../stack/ClientCertificateValidatorIT.java | 77 --- .../milo/opcua/stack/ClientServerTest.java | 648 ------------------ .../opcua/stack/StackIntegrationTest.java | 176 ----- .../client/tcp/OpcTcpTransportTest.java | 9 +- 19 files changed, 205 insertions(+), 1021 deletions(-) create mode 100644 opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/CipherFactory.java delete mode 100644 opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientCertificateValidatorIT.java delete mode 100644 opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientServerTest.java delete mode 100644 opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/StackIntegrationTest.java diff --git a/milo-examples/client-examples/src/main/java/org/eclipse/milo/examples/client/ClientExampleRunner.java b/milo-examples/client-examples/src/main/java/org/eclipse/milo/examples/client/ClientExampleRunner.java index 0b40487e95..f8ad2e4b7b 100644 --- a/milo-examples/client-examples/src/main/java/org/eclipse/milo/examples/client/ClientExampleRunner.java +++ b/milo-examples/client-examples/src/main/java/org/eclipse/milo/examples/client/ClientExampleRunner.java @@ -13,11 +13,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.Security; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.examples.server.ExampleServer; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.stack.core.Stack; @@ -32,11 +30,6 @@ public class ClientExampleRunner { - static { - // Required for SecurityPolicy.Aes256_Sha256_RsaPss - Security.addProvider(new BouncyCastleProvider()); - } - private final Logger logger = LoggerFactory.getLogger(getClass()); private final CompletableFuture future = new CompletableFuture<>(); diff --git a/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleServer.java b/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleServer.java index 9498578ac9..a8f08af5d6 100644 --- a/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleServer.java +++ b/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -19,7 +19,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyPair; -import java.security.Security; import java.security.cert.X509Certificate; import java.util.LinkedHashSet; import java.util.List; @@ -28,7 +27,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.opcua.sdk.server.EndpointConfig; import org.eclipse.milo.opcua.sdk.server.OpcUaServer; import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig; @@ -63,9 +61,6 @@ public class ExampleServer { private static final int TCP_BIND_PORT = 12686; static { - // Required for SecurityPolicy.Aes256_Sha256_RsaPss - Security.addProvider(new BouncyCastleProvider()); - try { NonceUtil.blockUntilSecureRandomSeeded(10, TimeUnit.SECONDS); } catch (ExecutionException | InterruptedException | TimeoutException e) { diff --git a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java index a6453cb3b4..4830b7c547 100644 --- a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java +++ b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java @@ -19,14 +19,12 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.security.KeyPair; -import java.security.Security; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Random; import java.util.Set; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.opcua.sdk.server.EndpointConfig; import org.eclipse.milo.opcua.sdk.server.OpcUaServer; import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig; @@ -60,11 +58,6 @@ public final class TestServer { - static { - // Required for SecurityPolicy.Aes256_Sha256_RsaPss - Security.addProvider(new BouncyCastleProvider()); - } - private final OpcUaServer opcUaServer; private final TestIdentityCertificate identityCert1; private final TestIdentityCertificate identityCert2; diff --git a/opc-ua-sdk/sdk-client/src/main/java/org/eclipse/milo/opcua/sdk/client/identity/UsernameProvider.java b/opc-ua-sdk/sdk-client/src/main/java/org/eclipse/milo/opcua/sdk/client/identity/UsernameProvider.java index 5b125b8d44..c34926450f 100644 --- a/opc-ua-sdk/sdk-client/src/main/java/org/eclipse/milo/opcua/sdk/client/identity/UsernameProvider.java +++ b/opc-ua-sdk/sdk-client/src/main/java/org/eclipse/milo/opcua/sdk/client/identity/UsernameProvider.java @@ -17,7 +17,6 @@ import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; @@ -31,6 +30,7 @@ import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.channel.SecureChannel; import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.SecurityAlgorithm; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType; @@ -39,6 +39,7 @@ import org.eclipse.milo.opcua.stack.core.types.structured.UserNameIdentityToken; import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy; import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; +import org.eclipse.milo.opcua.stack.core.util.CipherFactory; import org.eclipse.milo.opcua.stack.core.util.EndpointUtil; import org.eclipse.milo.opcua.stack.core.util.NonceUtil; @@ -250,7 +251,10 @@ public SignedIdentityToken getIdentityToken(EndpointDescription endpoint, ByteSt SecureChannel.getAsymmetricCipherTextBlockSize( certificate, securityPolicy.getAsymmetricEncryptionAlgorithm()); int blockCount = (buffer.readableBytes() + plainTextBlockSize - 1) / plainTextBlockSize; - Cipher cipher = getAndInitializeCipher(certificate, securityPolicy); + + Cipher cipher = + CipherFactory.createForEncryption( + securityPolicy.getAsymmetricEncryptionAlgorithm(), certificate.getPublicKey()); ByteBuffer plainTextNioBuffer = buffer.nioBuffer(); ByteBuffer cipherTextNioBuffer = @@ -290,14 +294,9 @@ private Cipher getAndInitializeCipher( assert (serverCertificate != null); - try { - String transformation = securityPolicy.getAsymmetricEncryptionAlgorithm().getTransformation(); - Cipher cipher = Cipher.getInstance(transformation); - cipher.init(Cipher.ENCRYPT_MODE, serverCertificate.getPublicKey()); - return cipher; - } catch (GeneralSecurityException e) { - throw new UaException(StatusCodes.Bad_SecurityChecksFailed, e); - } + SecurityAlgorithm algorithm = securityPolicy.getAsymmetricEncryptionAlgorithm(); + + return CipherFactory.createForEncryption(algorithm, serverCertificate.getPublicKey()); } @Override diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/identity/AbstractIdentityValidator.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/identity/AbstractIdentityValidator.java index 3e4a130237..7a891a576b 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/identity/AbstractIdentityValidator.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/identity/AbstractIdentityValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -35,6 +35,7 @@ import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy; import org.eclipse.milo.opcua.stack.core.types.structured.X509IdentityToken; import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; +import org.eclipse.milo.opcua.stack.core.util.CipherFactory; import org.eclipse.milo.opcua.stack.core.util.DigestUtil; public abstract class AbstractIdentityValidator implements IdentityValidator { @@ -200,7 +201,7 @@ protected byte[] decryptTokenData(Session session, SecurityAlgorithm algorithm, .getKeyPair(ByteString.of(DigestUtil.sha1(certificate.getEncoded()))) .orElseThrow(() -> new UaException(StatusCodes.Bad_SecurityChecksFailed)); - Cipher cipher = getCipher(algorithm, keyPair); + Cipher cipher = CipherFactory.createForDecryption(algorithm, keyPair.getPrivate()); for (int blockNumber = 0; blockNumber < blockCount; blockNumber++) { ((Buffer) passwordNioBuffer).limit(passwordNioBuffer.position() + cipherTextBlockSize); @@ -213,15 +214,4 @@ protected byte[] decryptTokenData(Session session, SecurityAlgorithm algorithm, return plainTextBytes; } - - private Cipher getCipher(SecurityAlgorithm algorithm, KeyPair keyPair) throws UaException { - try { - String transformation = algorithm.getTransformation(); - Cipher cipher = Cipher.getInstance(transformation); - cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); - return cipher; - } catch (GeneralSecurityException e) { - throw new UaException(StatusCodes.Bad_SecurityChecksFailed, e); - } - } } diff --git a/opc-ua-stack/stack-core/pom.xml b/opc-ua-stack/stack-core/pom.xml index 833944b69f..f232eb663f 100644 --- a/opc-ua-stack/stack-core/pom.xml +++ b/opc-ua-stack/stack-core/pom.xml @@ -29,11 +29,6 @@ - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - org.bouncycastle bcpkix-jdk18on diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkDecoder.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkDecoder.java index d063875a76..c0f18d1130 100644 --- a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkDecoder.java +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -23,6 +23,7 @@ import java.security.NoSuchAlgorithmException; import java.security.Signature; import java.security.SignatureException; +import java.security.spec.AlgorithmParameterSpec; import java.util.List; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; @@ -37,6 +38,7 @@ import org.eclipse.milo.opcua.stack.core.security.SecurityAlgorithm; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.util.BufferUtil; +import org.eclipse.milo.opcua.stack.core.util.CipherFactory; import org.eclipse.milo.opcua.stack.core.util.SignatureUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -311,15 +313,9 @@ public void readSecurityHeader(SecureChannel channel, ByteBuf chunkBuffer) { @Override public Cipher getCipher(SecureChannel channel) throws UaException { - try { - String transformation = - channel.getSecurityPolicy().getAsymmetricEncryptionAlgorithm().getTransformation(); - Cipher cipher = Cipher.getInstance(transformation); - cipher.init(Cipher.DECRYPT_MODE, channel.getKeyPair().getPrivate()); - return cipher; - } catch (GeneralSecurityException e) { - throw new UaException(StatusCodes.Bad_InternalError, e); - } + SecurityAlgorithm algorithm = channel.getSecurityPolicy().getAsymmetricEncryptionAlgorithm(); + + return CipherFactory.createForDecryption(algorithm, channel.getKeyPair().getPrivate()); } @Override @@ -336,6 +332,8 @@ public int getSignatureSize(SecureChannel channel) { public void verifyChunk(SecureChannel channel, ByteBuf chunkBuffer) throws UaException { String transformation = channel.getSecurityPolicy().getAsymmetricSignatureAlgorithm().getTransformation(); + AlgorithmParameterSpec parameterSpec = + channel.getSecurityPolicy().getAsymmetricSignatureAlgorithm().getAlgorithmParameterSpec(); int signatureSize = channel.getRemoteAsymmetricSignatureSize(); ByteBuffer chunkNioBuffer = chunkBuffer.nioBuffer(0, chunkBuffer.writerIndex()); @@ -345,6 +343,10 @@ public void verifyChunk(SecureChannel channel, ByteBuf chunkBuffer) throws UaExc try { Signature signature = Signature.getInstance(transformation); + if (parameterSpec != null) { + signature.setParameter(parameterSpec); + } + signature.initVerify(channel.getRemoteCertificate().getPublicKey()); signature.update(chunkNioBuffer); @@ -361,6 +363,8 @@ public void verifyChunk(SecureChannel channel, ByteBuf chunkBuffer) throws UaExc throw new UaException(StatusCodes.Bad_ApplicationSignatureInvalid, e); } catch (InvalidKeyException e) { throw new UaException(StatusCodes.Bad_CertificateInvalid, e); + } catch (GeneralSecurityException e) { + throw new UaException(StatusCodes.Bad_SecurityChecksFailed, e); } } diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkEncoder.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkEncoder.java index a2ba4b5d22..f35b02076c 100644 --- a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkEncoder.java +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/channel/ChunkEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -34,6 +34,7 @@ import org.eclipse.milo.opcua.stack.core.security.SecurityAlgorithm; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.util.BufferUtil; +import org.eclipse.milo.opcua.stack.core.util.CipherFactory; import org.eclipse.milo.opcua.stack.core.util.LongSequence; import org.eclipse.milo.opcua.stack.core.util.SignatureUtil; @@ -301,15 +302,9 @@ public Cipher getCipher(SecureChannel channel) throws UaException { assert (remoteCertificate != null); - try { - String transformation = - channel.getSecurityPolicy().getAsymmetricEncryptionAlgorithm().getTransformation(); - Cipher cipher = Cipher.getInstance(transformation); - cipher.init(Cipher.ENCRYPT_MODE, remoteCertificate.getPublicKey()); - return cipher; - } catch (GeneralSecurityException e) { - throw new UaException(StatusCodes.Bad_SecurityChecksFailed, e); - } + SecurityAlgorithm algorithm = channel.getSecurityPolicy().getAsymmetricEncryptionAlgorithm(); + + return CipherFactory.createForEncryption(algorithm, remoteCertificate.getPublicKey()); } @Override diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/security/SecurityAlgorithm.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/security/SecurityAlgorithm.java index 126882c363..124f0c865f 100644 --- a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/security/SecurityAlgorithm.java +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/security/SecurityAlgorithm.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,10 +12,16 @@ import java.security.MessageDigest; import java.security.Signature; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; import javax.crypto.Cipher; import javax.crypto.Mac; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; +import org.jspecify.annotations.Nullable; public enum SecurityAlgorithm { None("", ""), @@ -41,9 +47,10 @@ public enum SecurityAlgorithm { /** * Asymmetric Signature; transformation to be used with {@link Signature#getInstance(String)}. * - *

Requires Bouncy Castle installed as a Security Provider. + *

Uses custom {@link PSSParameterSpec} with SHA-256 for both digest and MGF1, 32-byte salt + * length, and standard trailer field. See {@link #getAlgorithmParameterSpec()}. */ - RsaSha256Pss("http://opcfoundation.org/UA/security/rsa-pss-sha2-256", "SHA256withRSA/PSS"), + RsaSha256Pss("http://opcfoundation.org/UA/security/rsa-pss-sha2-256", "RSASSA-PSS"), /** Asymmetric Encryption; transformation to be used with {@link Cipher#getInstance(String)}. */ Rsa15("http://www.w3.org/2001/04/xmlenc#rsa-1_5", "RSA/ECB/PKCS1Padding"), @@ -54,18 +61,12 @@ public enum SecurityAlgorithm { /** * Asymmetric Encryption; transformation to be used with {@link Cipher#getInstance(String)}. * - *

Important note: the transformation used is "RSA/ECB/OAEPWithSHA256AndMGF1Padding" as opposed - * to "RSA/ECB/OAEPWithSHA-256AndMGF1Padding". - * - *

While similar, the former is provided by Bouncy Castle whereas the latter is provided by - * SunJCE. - * - *

This is important because the BC version uses SHA256 in the padding while the SunJCE version - * uses Sha1. + *

Uses custom {@link OAEPParameterSpec} with SHA-256 for both OAEP hash and MGF1 to ensure + * consistent behavior across JCE providers. See {@link #getAlgorithmParameterSpec()}. */ RsaOaepSha256( "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256", - "RSA/ECB/OAEPWithSHA256AndMGF1Padding"), + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"), /** Asymmetric Key Wrap */ KwRsa15("http://www.w3.org/2001/04/xmlenc#rsa-1_5", ""), @@ -111,6 +112,44 @@ public String getTransformation() { return transformation; } + /** + * Returns algorithm-specific parameters required for certain security algorithms. + * + *

Currently provides custom parameter specifications for: + * + *

    + *
  • {@link #RsaOaepSha256} - Returns {@link OAEPParameterSpec} configured with SHA-256 for + * both the OAEP hash algorithm and MGF1, ensuring consistent SHA-256 usage across JCE + * providers. + *
  • {@link #RsaSha256Pss} - Returns {@link PSSParameterSpec} configured with SHA-256 for both + * digest and MGF1, 32-byte salt length (matching SHA-256 output), and standard trailer + * field. + *
+ * + * @return the algorithm parameter specification for this algorithm, or {@code null} if no custom + * parameters are required. + */ + public @Nullable AlgorithmParameterSpec getAlgorithmParameterSpec() { + if (this == SecurityAlgorithm.RsaOaepSha256) { + // Specify OAEP parameters for SHA-256 for both OAEP hash and MGF1 + return new OAEPParameterSpec( + "SHA-256", // OAEP hash algorithm + "MGF1", // Mask Generation Function algorithm + new MGF1ParameterSpec("SHA-256"), // MGF1 hash algorithm + PSource.PSpecified.DEFAULT // PSource (empty label) + ); + } else if (this == RsaSha256Pss) { + return new PSSParameterSpec( + "SHA-256", // Digest algorithm + "MGF1", // Mask Generation Function algorithm + new MGF1ParameterSpec("SHA-256"), // MGF1 hash algorithm + 32, // Salt length (matches SHA-256 output, 256 bits = 32 bytes) + 1 // Trailer field (the standard default) + ); + } + return null; + } + public static SecurityAlgorithm fromUri(String securityAlgorithmUri) throws UaException { for (SecurityAlgorithm algorithm : values()) { if (algorithm.getUri().equals(securityAlgorithmUri)) { diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/CipherFactory.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/CipherFactory.java new file mode 100644 index 0000000000..7bf52ee0d2 --- /dev/null +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/CipherFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 the Eclipse Milo Authors + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.milo.opcua.stack.core.util; + +import java.security.*; +import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.Cipher; +import org.eclipse.milo.opcua.stack.core.StatusCodes; +import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.SecurityAlgorithm; + +/** + * Factory for creating {@link Cipher} instances configured for OPC UA security operations. + * + *

This utility class creates and initializes ciphers for asymmetric encryption and decryption + * used in OPC UA secure channels and identity token encryption. The ciphers are configured based on + * the {@link SecurityAlgorithm} which provides the transformation and optional algorithm + * parameters. + */ +public class CipherFactory { + + private CipherFactory() {} + + /** + * Creates a cipher initialized for encryption using the specified algorithm and public key. + * + *

The cipher is configured in {@link Cipher#ENCRYPT_MODE} and initialized with the provided + * public key. If the security algorithm specifies algorithm parameters, they are included in the + * initialization. + * + * @param algorithm the security algorithm defining the transformation and optional parameters. + * @param publicKey the public key to use for encryption. + * @return a cipher initialized for encryption. + * @throws UaException with status code {@link StatusCodes#Bad_SecurityChecksFailed} if cipher + * initialization fails. + */ + public static Cipher createForEncryption(SecurityAlgorithm algorithm, PublicKey publicKey) + throws UaException { + + String transformation = algorithm.getTransformation(); + AlgorithmParameterSpec parameterSpec = algorithm.getAlgorithmParameterSpec(); + + try { + Cipher cipher = Cipher.getInstance(transformation); + + if (parameterSpec != null) { + cipher.init(Cipher.ENCRYPT_MODE, publicKey, parameterSpec); + } else { + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + } + + return cipher; + } catch (GeneralSecurityException e) { + throw new UaException(StatusCodes.Bad_SecurityChecksFailed, "failed to initialize cipher", e); + } + } + + /** + * Creates a cipher initialized for decryption using the specified algorithm and private key. + * + *

The cipher is configured in {@link Cipher#DECRYPT_MODE} and initialized with the provided + * private key. If the security algorithm specifies algorithm parameters, they are included in the + * initialization. + * + * @param algorithm the security algorithm defining the transformation and optional parameters. + * @param privateKey the private key to use for decryption. + * @return a cipher initialized for decryption. + * @throws UaException with status code {@link StatusCodes#Bad_SecurityChecksFailed} if cipher + * initialization fails. + */ + public static Cipher createForDecryption(SecurityAlgorithm algorithm, PrivateKey privateKey) + throws UaException { + + try { + String transformation = algorithm.getTransformation(); + AlgorithmParameterSpec parameterSpec = algorithm.getAlgorithmParameterSpec(); + + Cipher cipher = Cipher.getInstance(transformation); + + if (parameterSpec != null) { + cipher.init(Cipher.DECRYPT_MODE, privateKey, parameterSpec); + } else { + cipher.init(Cipher.DECRYPT_MODE, privateKey); + } + + return cipher; + } catch (GeneralSecurityException e) { + throw new UaException(StatusCodes.Bad_SecurityChecksFailed, "failed to initialize cipher", e); + } + } +} diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SelfSignedCertificateGenerator.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SelfSignedCertificateGenerator.java index c604678104..01ca760a78 100644 --- a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SelfSignedCertificateGenerator.java +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SelfSignedCertificateGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -37,7 +37,6 @@ import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.jspecify.annotations.Nullable; @@ -145,9 +144,7 @@ public X509Certificate generateSelfSigned( addSubjectAlternativeNames(certificateBuilder, keyPair, applicationUri, dnsNames, ipAddresses); ContentSigner contentSigner = - new JcaContentSignerBuilder(signatureAlgorithm) - .setProvider(new BouncyCastleProvider()) - .build(keyPair.getPrivate()); + new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner); diff --git a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SignatureUtil.java b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SignatureUtil.java index e738324e3e..2e10291df2 100644 --- a/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SignatureUtil.java +++ b/opc-ua-stack/stack-core/src/main/java/org/eclipse/milo/opcua/stack/core/util/SignatureUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,12 +12,11 @@ import java.nio.ByteBuffer; import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; -import java.security.SignatureException; import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; import java.util.HashMap; import java.util.Map; import javax.crypto.Mac; @@ -43,9 +42,15 @@ public static byte[] sign( throws UaException { String transformation = securityAlgorithm.getTransformation(); + AlgorithmParameterSpec parameterSpec = securityAlgorithm.getAlgorithmParameterSpec(); try { Signature signature = Signature.getInstance(transformation); + + if (parameterSpec != null) { + signature.setParameter(parameterSpec); + } + signature.initSign(privateKey); for (ByteBuffer buffer : buffers) { @@ -76,7 +81,15 @@ public static void verify( throws UaException { try { - Signature signature = Signature.getInstance(algorithm.getTransformation()); + String transformation = algorithm.getTransformation(); + AlgorithmParameterSpec parameterSpec = algorithm.getAlgorithmParameterSpec(); + + Signature signature = Signature.getInstance(transformation); + + if (parameterSpec != null) { + signature.setParameter(parameterSpec); + } + signature.initVerify(certificate); signature.update(dataBytes); @@ -84,9 +97,9 @@ public static void verify( if (!signature.verify(signatureBytes)) { throw new UaException(StatusCodes.Bad_SecurityChecksFailed, "could not verify signature"); } - } catch (NoSuchAlgorithmException | SignatureException e) { + } catch (NoSuchAlgorithmException e) { throw new UaException(StatusCodes.Bad_InternalError, e); - } catch (InvalidKeyException e) { + } catch (GeneralSecurityException e) { throw new UaException(StatusCodes.Bad_SecurityChecksFailed, e); } } diff --git a/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CaSignedCertificateBuilder.java b/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CaSignedCertificateBuilder.java index 15b3fa723e..5aa69f8704 100644 --- a/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CaSignedCertificateBuilder.java +++ b/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CaSignedCertificateBuilder.java @@ -40,7 +40,6 @@ import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.jspecify.annotations.Nullable; @@ -364,9 +363,7 @@ public X509Certificate build() throws Exception { } ContentSigner contentSigner = - new JcaContentSignerBuilder(signatureAlgorithm) - .setProvider(new BouncyCastleProvider()) - .build(issuerPrivateKey); + new JcaContentSignerBuilder(signatureAlgorithm).build(issuerPrivateKey); X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner); diff --git a/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CertificateValidationUtilTest.java b/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CertificateValidationUtilTest.java index 590396d6fc..4e5f94bf56 100644 --- a/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CertificateValidationUtilTest.java +++ b/opc-ua-stack/stack-core/src/test/java/org/eclipse/milo/opcua/stack/core/util/validation/CertificateValidationUtilTest.java @@ -31,7 +31,6 @@ import java.security.KeyPair; import java.security.PrivateKey; -import java.security.Security; import java.security.cert.PKIXCertPathBuilderResult; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; @@ -44,7 +43,6 @@ import org.bouncycastle.cert.X509CRLHolder; import org.bouncycastle.cert.X509v2CRLBuilder; import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; @@ -57,10 +55,6 @@ public class CertificateValidationUtilTest { - static { - Security.addProvider(new BouncyCastleProvider()); - } - private static TestCertificates testCertificates; private static X509Certificate caIntermediate; private static X509Certificate caRoot; @@ -412,14 +406,10 @@ private X509CRL generateCrl( JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption"); - contentSignerBuilder.setProvider("BC"); - X509CRLHolder crlHolder = builder.build(contentSignerBuilder.build(caPrivateKey)); JcaX509CRLConverter converter = new JcaX509CRLConverter(); - converter.setProvider("BC"); - return converter.getCRL(crlHolder); } diff --git a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ChunkSerializationTest.java b/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ChunkSerializationTest.java index c9adc4d143..dab82875a6 100644 --- a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ChunkSerializationTest.java +++ b/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ChunkSerializationTest.java @@ -17,10 +17,8 @@ import io.netty.buffer.ByteBuf; import io.netty.util.ReferenceCountUtil; -import java.security.Security; import java.util.ArrayList; import java.util.List; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.opcua.stack.core.channel.ChannelParameters; import org.eclipse.milo.opcua.stack.core.channel.ChunkDecoder; import org.eclipse.milo.opcua.stack.core.channel.ChunkEncoder; @@ -43,11 +41,6 @@ public class ChunkSerializationTest extends SecureChannelFixture { - static { - // Required for SecurityPolicy.Aes256_Sha256_RsaPss - Security.addProvider(new BouncyCastleProvider()); - } - private static final Logger LOGGER = LoggerFactory.getLogger(ChunkSerializationTest.class); private final ChannelParameters smallParameters = diff --git a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientCertificateValidatorIT.java b/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientCertificateValidatorIT.java deleted file mode 100644 index 9ffb799676..0000000000 --- a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientCertificateValidatorIT.java +++ /dev/null @@ -1,77 +0,0 @@ -/// * -// * Copyright (c) 2019 the Eclipse Milo Authors -// * -// * This program and the accompanying materials are made -// * available under the terms of the Eclipse Public License 2.0 -// * which is available at https://www.eclipse.org/legal/epl-2.0/ -// * -// * SPDX-License-Identifier: EPL-2.0 -// */ -// -// package org.eclipse.milo.opcua.stack; -// -// import java.security.cert.X509Certificate; -// import java.util.List; -// import java.util.concurrent.CountDownLatch; -// import java.util.concurrent.TimeUnit; -// -// import org.eclipse.milo.opcua.stack.client.UaStackClientConfigBuilder; -// import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator; -// import org.eclipse.milo.opcua.stack.core.UaException; -// import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; -// import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; -// import org.slf4j.Logger; -// import org.slf4j.LoggerFactory; -// import org.testng.annotations.Test; -// -// import static org.testng.Assert.assertTrue; -// -// public class ClientCertificateValidatorIT extends StackIntegrationTest { -// -// private final Logger logger = LoggerFactory.getLogger(getClass()); -// -// private final CountDownLatch latch = new CountDownLatch(1); -// -// private final ClientCertificateValidator validator = new ClientCertificateValidator() { -// -// @Override -// public void validateCertificateChain(List certificateChain) throws -// UaException { -// X509Certificate certificate = certificateChain.get(0); -// logger.info("verifyTrustChain: {}", certificate.getSubjectX500Principal()); -// latch.countDown(); -// } -// -// @Override -// public void validateCertificateChain( -// List certificateChain, -// String applicationUri, -// String... validHostNames -// ) throws UaException { -// -// validateCertificateChain(certificateChain); -// } -// -// }; -// -// @Test -// public void testClientCertificateValidatorIsCalled() throws InterruptedException { -// assertTrue(latch.await(10, TimeUnit.SECONDS), "latch not obtained!"); -// } -// -// @Override -// protected UaStackClientConfigBuilder configureClient(UaStackClientConfigBuilder builder) { -// return builder -// .setCertificate(clientCertificate) -// .setCertificateValidator(validator); -// } -// -// @Override -// protected EndpointDescription selectEndpoint(List endpoints) { -// return endpoints.stream() -// .filter(e -> !SecurityPolicy.None.getUri().equals(e.getSecurityPolicyUri())) -// .findFirst() -// .orElseThrow(() -> new RuntimeException("no secure endpoint found!")); -// } -// -// } diff --git a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientServerTest.java b/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientServerTest.java deleted file mode 100644 index 16087d155a..0000000000 --- a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/ClientServerTest.java +++ /dev/null @@ -1,648 +0,0 @@ -/// * -// * Copyright (c) 2022 the Eclipse Milo Authors -// * -// * This program and the accompanying materials are made -// * available under the terms of the Eclipse Public License 2.0 -// * which is available at https://www.eclipse.org/legal/epl-2.0/ -// * -// * SPDX-License-Identifier: EPL-2.0 -// */ -// -// package org.eclipse.milo.opcua.stack; -// -// import java.security.Security; -// import java.security.cert.X509Certificate; -// import java.util.ArrayList; -// import java.util.Collections; -// import java.util.LinkedHashSet; -// import java.util.List; -// import java.util.Set; -// import java.util.UUID; -// import java.util.concurrent.CompletableFuture; -// -// import org.bouncycastle.jce.provider.BouncyCastleProvider; -// import org.eclipse.milo.opcua.stack.client.DiscoveryClient; -// import org.eclipse.milo.opcua.stack.client.UaStackClient; -// import org.eclipse.milo.opcua.stack.client.UaStackClientConfig; -// import org.eclipse.milo.opcua.stack.core.AttributeId; -// import org.eclipse.milo.opcua.stack.core.Stack; -// import org.eclipse.milo.opcua.stack.core.StatusCodes; -// import org.eclipse.milo.opcua.stack.core.UaException; -// import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; -// import org.eclipse.milo.opcua.stack.core.transport.TransportProfile; -// import org.eclipse.milo.opcua.stack.core.types.UaResponseMessageType; -// import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; -// import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; -// import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; -// import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId; -// import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; -// import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; -// import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; -// import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; -// import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; -// import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; -// import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement; -// import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; -// import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; -// import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; -// import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType; -// import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; -// import org.eclipse.milo.opcua.stack.core.types.structured.ReadRequest; -// import org.eclipse.milo.opcua.stack.core.types.structured.ReadResponse; -// import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; -// import org.eclipse.milo.opcua.stack.core.types.structured.RequestHeader; -// import org.eclipse.milo.opcua.stack.core.types.structured.ResponseHeader; -// import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy; -// import org.eclipse.milo.opcua.stack.core.util.FutureUtils; -// import org.eclipse.milo.opcua.stack.server.EndpointConfiguration; -// import org.eclipse.milo.opcua.stack.server.UaStackServer; -// import org.eclipse.milo.opcua.stack.server.UaStackServerConfig; -// import org.slf4j.Logger; -// import org.slf4j.LoggerFactory; -// import org.testng.annotations.AfterSuite; -// import org.testng.annotations.BeforeSuite; -// import org.testng.annotations.DataProvider; -// import org.testng.annotations.Test; -// -// import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; -// import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; -// import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong; -// import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; -// import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.a; -// import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.l; -// import static org.testng.Assert.assertEquals; -// import static org.testng.Assert.assertThrows; -// import static org.testng.Assert.fail; -// -// public class ClientServerTest extends SecurityFixture { -// -// static { -// Security.addProvider(new BouncyCastleProvider()); -// -// Stack.ConnectionLimits.RATE_LIMIT_ENABLED = false; -// } -// -// private static final UInteger DEFAULT_TIMEOUT_HINT = uint(60000); -// -// @DataProvider -// public Object[][] getVariants() { -// return new Object[][]{ -// {new Variant(true)}, -// {new Variant((byte) 1)}, -// {new Variant(ubyte(1))}, -// {new Variant((short) 1)}, -// {new Variant(ushort(1))}, -// {new Variant(1)}, -// {new Variant(uint(1))}, -// {new Variant(1L)}, -// {new Variant(ulong(1L))}, -// {new Variant(3.14f)}, -// {new Variant(6.12d)}, -// {new Variant("hello, world")}, -// {new Variant(DateTime.now())}, -// {new Variant(UUID.randomUUID())}, -// {new Variant(ByteString.of(new byte[]{1, 2, 3, 4}))}, -// {new Variant(new XmlElement("hello"))}, -// {new Variant(new NodeId(0, 42))}, -// {new Variant(new ExpandedNodeId(ushort(1), "uri", uint(42), uint(1)))}, -// {new Variant(StatusCode.GOOD)}, -// {new Variant(new QualifiedName(0, "QualifiedName"))}, -// {new Variant(LocalizedText.english("LocalizedText"))}, -// {new Variant(ExtensionObject.encode( -// new TestEncodingContext(), -// new ReadValueId(NodeId.NULL_VALUE, uint(1), null, new QualifiedName(0, -// "DataEncoding")) -// ))}, -// }; -// } -// -// private Logger logger = LoggerFactory.getLogger(getClass()); -// -// private EndpointDescription[] endpoints; -// -// private UaStackServer server; -// -// @BeforeSuite -// public void setUpClientServer() throws Exception { -// super.setUp(); -// -// UaStackServerConfig config = UaStackServerConfig.builder() -// .setCertificateManager(serverCertificateManager) -// .setCertificateValidator(serverCertificateValidator) -// .setEndpoints(createEndpointConfigurations(serverCertificate)) -// .build(); -// -// server = new UaStackServer(config); -// -// setReadRequestHandler(new Variant(42)); -// -// server.startup().get(); -// -// endpoints = DiscoveryClient.getEndpoints("opc.tcp://localhost:12685/test") -// .get() -// .toArray(new EndpointDescription[0]); -// } -// -// @AfterSuite -// public void tearDownClientServer() throws Exception { -// server.shutdown().get(); -// } -// -// private void setReadRequestHandler(Variant variant) { -// server.addServiceHandler("/test", ReadRequest.TYPE_ID, service -> { -// ReadRequest request = (ReadRequest) service.getRequest(); -// -// ResponseHeader header = new ResponseHeader( -// DateTime.now(), -// request.getRequestHeader().getRequestHandle(), -// StatusCode.GOOD, -// null, -// null, -// null -// ); -// -// List nodesToRead = List.of(request.getNodesToRead()); -// List results = Collections.nCopies(nodesToRead.size(), new -// DataValue(variant)); -// -// ReadResponse response = new ReadResponse(header, a(results, DataValue.class), null); -// -// service.setResponse(response); -// }); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_NoSecurity(Variant input) throws Exception { -// EndpointDescription endpoint = endpoints[0]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic128Rsa15_Sign(Variant input) throws -// Exception { -// EndpointDescription endpoint = endpoints[1]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic256_Sign(Variant input) throws Exception -// { -// EndpointDescription endpoint = endpoints[2]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic256Sha256_Sign(Variant input) throws -// Exception { -// EndpointDescription endpoint = endpoints[3]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic128Rsa15_SignAndEncrypt(Variant input) -// throws Exception { -// EndpointDescription endpoint = endpoints[4]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic256_SignAndEncrypt(Variant input) throws -// Exception { -// EndpointDescription endpoint = endpoints[5]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test(dataProvider = "getVariants") -// public void testClientServerRoundTrip_TestStack_Basic256Sha256_SignAndEncrypt(Variant input) -// throws Exception { -// EndpointDescription endpoint = endpoints[6]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// connectAndTest(input, client); -// } -// -// @Test -// public void testClientStateMachine() throws Exception { -// EndpointDescription endpoint = endpoints[0]; -// -// Variant input = new Variant(42); -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// for (int i = 0; i < 1000; i++) { -// client.connect().get(); -// -// RequestHeader header = new RequestHeader( -// NodeId.NULL_VALUE, -// DateTime.now(), -// uint(i), -// uint(0), -// null, -// DEFAULT_TIMEOUT_HINT, -// null -// ); -// -// ReadRequest request = new ReadRequest( -// header, -// 0.0, -// TimestampsToReturn.Neither, -// new ReadValueId[]{ -// new ReadValueId( -// NodeId.NULL_VALUE, -// AttributeId.Value.uid(), -// null, -// null) -// } -// ); -// -// logger.debug("sending request: {}", request); -// UaResponseMessageType response = client.sendRequest(request).get(); -// logger.debug("got response: {}", response); -// -// client.disconnect().get(); -// } -// } -// -// @Test -// public void testClientDisconnect() throws Exception { -// EndpointDescription endpoint = endpoints[0]; -// Variant input = new Variant(42); -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// client.connect().get(); -// -// RequestHeader header = new RequestHeader( -// NodeId.NULL_VALUE, -// DateTime.now(), -// uint(0), -// uint(0), -// null, -// DEFAULT_TIMEOUT_HINT, -// null -// ); -// -// ReadRequest request = new ReadRequest( -// header, -// 0.0, -// TimestampsToReturn.Neither, -// new ReadValueId[]{ -// new ReadValueId( -// NodeId.NULL_VALUE, -// AttributeId.Value.uid(), -// null, -// null) -// } -// ); -// -// logger.info("sending request: {}", request); -// UaResponseMessageType response0 = client.sendRequest(request).get(); -// logger.info("got response: {}", response0); -// -// client.disconnect().get(); -// -// assertThrows(() -> client.sendRequest(request).get()); -// } -// -// @Test -// public void testClientReconnect() throws Exception { -// EndpointDescription endpoint = endpoints[0]; -// Variant input = new Variant(42); -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}, input={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode(), -// input); -// -// UaStackClient client = createClient(endpoint); -// -// client.connect().get(); -// -// RequestHeader header = new RequestHeader( -// NodeId.NULL_VALUE, -// DateTime.now(), -// uint(0), -// uint(0), -// null, -// DEFAULT_TIMEOUT_HINT, -// null -// ); -// -// ReadRequest request = new ReadRequest( -// header, -// 0.0, -// TimestampsToReturn.Neither, -// new ReadValueId[]{ -// new ReadValueId( -// NodeId.NULL_VALUE, -// AttributeId.Value.uid(), -// null, -// null) -// } -// ); -// -// logger.info("sending request: {}", request); -// UaResponseMessageType response0 = client.sendRequest(request).get(); -// logger.info("got response: {}", response0); -// -// logger.info("initiating a reconnect by closing channel in server..."); -// server.getConnectedChannels().forEach(c -> { -// try { -// c.close().await(); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// } -// }); -// -// logger.info("sending request: {}", request); -// try { -// UaResponseMessageType response1 = client.sendRequest(request).get(); -// logger.info("got response: {}", response1); -// } catch (Exception e) { -// // try again because close() above is a race condition -// UaResponseMessageType response1 = client.sendRequest(request).get(); -// logger.info("got response: {}", response1); -// } -// -// client.disconnect().get(); -// } -// -// @Test -// public void testClientTimeout() throws Exception { -// EndpointDescription endpoint = endpoints[0]; -// -// logger.info("SecurityPolicy={}, MessageSecurityMode={}", -// SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()), endpoint.getSecurityMode()); -// -// UaStackClientConfig config = UaStackClientConfig.builder() -// .setEndpoint(endpoint) -// .setKeyPair(clientKeyPair) -// .setCertificate(clientCertificate) -// .build(); -// -// UaStackClient client = UaStackClient.create(config); -// client.connect().get(); -// -// server.addServiceHandler("/test", ReadRequest.TYPE_ID, service -> { -// // intentionally do nothing so the request can timeout -// logger.info("received {}; ignoring...", service.getRequest()); -// }); -// -// RequestHeader header = new RequestHeader( -// NodeId.NULL_VALUE, -// DateTime.now(), -// uint(0), -// uint(0), -// null, -// uint(1000), -// null -// ); -// -// ReadRequest request = new ReadRequest( -// header, -// 0.0, -// TimestampsToReturn.Neither, -// new ReadValueId[]{ -// new ReadValueId( -// NodeId.NULL_VALUE, -// AttributeId.Value.uid(), -// null, -// null) -// } -// ); -// -// try { -// client.sendRequest(request).get(); -// -// fail("expected response to timeout"); -// } catch (Throwable t) { -// StatusCode statusCode = UaException -// .extractStatusCode(t) -// .orElse(StatusCode.BAD); -// -// assertEquals(statusCode.getValue(), StatusCodes.Bad_Timeout); -// } -// } -// -// private UaStackClient createClient(EndpointDescription endpoint) throws UaException { -// UaStackClientConfig config = UaStackClientConfig.builder() -// .setEndpoint(endpoint) -// .setKeyPair(clientKeyPair) -// .setCertificate(clientCertificate) -// .build(); -// -// return UaStackClient.create(config); -// } -// -// private void connectAndTest(Variant input, UaStackClient client) throws InterruptedException, -// java.util.concurrent.ExecutionException { -// setReadRequestHandler(input); -// -// client.connect().get(); -// -// var responses = new ArrayList>(); -// -// for (int i = 0; i < 100; i++) { -// RequestHeader header = new RequestHeader( -// NodeId.NULL_VALUE, -// DateTime.now(), -// uint(i), -// uint(0), -// null, -// uint(10000), -// null -// ); -// -// ReadRequest request = new ReadRequest( -// header, -// 0.0, -// TimestampsToReturn.Neither, -// new ReadValueId[]{ -// new ReadValueId( -// NodeId.NULL_VALUE, -// AttributeId.Value.uid(), -// null, -// null) -// } -// ); -// -// responses.add( -// client.sendRequest(request) -// .thenApply(ReadResponse.class::cast)); -// } -// -// -// CompletableFuture.allOf(responses.toArray(new CompletableFuture[0])).get(); -// -// FutureUtils.sequence(responses).get().forEach(response -> { -// Variant value = List.of(response.getResults()).get(0).getValue(); -// -// assertEquals(value, input); -// }); -// -// client.disconnect().get(); -// } -// -// private Set createEndpointConfigurations(X509Certificate certificate) { -// Set endpointConfigurations = new LinkedHashSet<>(); -// -// List bindAddresses = new ArrayList<>(); -// bindAddresses.add("localhost"); -// -// Set hostnames = new LinkedHashSet<>(); -// hostnames.add("localhost"); -// -// for (String bindAddress : bindAddresses) { -// for (String hostname : hostnames) { -// EndpointConfiguration.Builder builder = EndpointConfiguration.newBuilder() -// .setBindAddress(bindAddress) -// .setHostname(hostname) -// .setPath("/test") -// .setCertificate(certificate) -// .addTokenPolicies( -// USER_TOKEN_POLICY_ANONYMOUS); -// -// -// /* No Security */ -// EndpointConfiguration.Builder noSecurityBuilder = builder.copy() -// .setSecurityPolicy(SecurityPolicy.None) -// .setSecurityMode(MessageSecurityMode.None); -// -// endpointConfigurations.add(buildTcpEndpoint(noSecurityBuilder)); -// -// /* Basic128Rsa15 */ -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic128Rsa15) -// .setSecurityMode(MessageSecurityMode.Sign)) -// ); -// -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic128Rsa15) -// .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) -// ); -// -// /* Basic256 */ -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic256) -// .setSecurityMode(MessageSecurityMode.Sign)) -// ); -// -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic256) -// .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) -// ); -// -// /* Basic256Sha256 */ -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic256Sha256) -// .setSecurityMode(MessageSecurityMode.Sign)) -// ); -// -// endpointConfigurations.add(buildTcpEndpoint( -// builder.copy() -// .setSecurityPolicy(SecurityPolicy.Basic256Sha256) -// .setSecurityMode(MessageSecurityMode.SignAndEncrypt)) -// ); -// -// /* -// * It's good practice to provide a discovery-specific endpoint with no security. -// * It's required practice if all regular endpoints have security configured. -// * -// * Usage of the "/discovery" suffix is defined by OPC UA Part 6: -// * -// * Each OPC UA Server Application implements the Discovery Service Set. If the OPC -// UA Server requires a -// * different address for this Endpoint it shall create the address by appending -// the path "/discovery" to -// * its base address. -// */ -// -// EndpointConfiguration.Builder discoveryBuilder = builder.copy() -// .setPath("/example/discovery") -// .setSecurityPolicy(SecurityPolicy.None) -// .setSecurityMode(MessageSecurityMode.None); -// -// endpointConfigurations.add(buildTcpEndpoint(discoveryBuilder)); -// } -// } -// -// return endpointConfigurations; -// } -// -// private static EndpointConfiguration buildTcpEndpoint(EndpointConfiguration.Builder base) { -// return base.copy() -// .setTransportProfile(TransportProfile.TCP_UASC_UABINARY) -// .setBindPort(12685) -// .build(); -// } -// -// /** -// * A {@link UserTokenPolicy} for anonymous access. -// */ -// private static final UserTokenPolicy USER_TOKEN_POLICY_ANONYMOUS = new UserTokenPolicy( -// "anonymous", -// UserTokenType.Anonymous, -// null, -// null, -// null -// ); -// -// } diff --git a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/StackIntegrationTest.java b/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/StackIntegrationTest.java deleted file mode 100644 index 6b3c72dc33..0000000000 --- a/opc-ua-stack/stack-tests/src/test/java/org/eclipse/milo/opcua/stack/StackIntegrationTest.java +++ /dev/null @@ -1,176 +0,0 @@ -/// * -// * Copyright (c) 2022 the Eclipse Milo Authors -// * -// * This program and the accompanying materials are made -// * available under the terms of the Eclipse Public License 2.0 -// * which is available at https://www.eclipse.org/legal/epl-2.0/ -// * -// * SPDX-License-Identifier: EPL-2.0 -// */ -// -// package org.eclipse.milo.opcua.stack; -// -// import java.security.Security; -// import java.util.ArrayList; -// import java.util.LinkedHashSet; -// import java.util.List; -// import java.util.Random; -// import java.util.Set; -// -// import org.bouncycastle.jce.provider.BouncyCastleProvider; -// import org.eclipse.milo.opcua.stack.client.DiscoveryClient; -// import org.eclipse.milo.opcua.stack.client.UaStackClient; -// import org.eclipse.milo.opcua.stack.client.UaStackClientConfig; -// import org.eclipse.milo.opcua.stack.client.UaStackClientConfigBuilder; -// import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; -// import org.eclipse.milo.opcua.stack.core.transport.TransportProfile; -// import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; -// import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType; -// import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; -// import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy; -// import org.eclipse.milo.opcua.stack.server.EndpointConfiguration; -// import org.eclipse.milo.opcua.stack.server.UaStackServer; -// import org.eclipse.milo.opcua.stack.server.UaStackServerConfig; -// import org.eclipse.milo.opcua.stack.server.UaStackServerConfigBuilder; -// import org.slf4j.Logger; -// import org.slf4j.LoggerFactory; -// import org.testng.annotations.AfterSuite; -// import org.testng.annotations.BeforeSuite; -// import org.testng.annotations.Test; -// -// import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; -// -// public abstract class StackIntegrationTest extends SecurityFixture { -// -// static { -// Security.addProvider(new BouncyCastleProvider()); -// } -// -// private static final UserTokenPolicy USER_TOKEN_POLICY_ANONYMOUS = new UserTokenPolicy( -// "anonymous", -// UserTokenType.Anonymous, -// null, -// null, -// null -// ); -// -// protected final Logger logger = LoggerFactory.getLogger(getClass()); -// -// private final int tcpBindPort = new Random().nextInt(64000) + 1000; -// protected UaStackClient stackClient; -// protected UaStackServer stackServer; -// -// @BeforeSuite -// public void setUpClientServer() throws Exception { -// super.setUp(); -// -// int tcpBindPort = getTcpBindPort(); -// -// List bindAddresses = new ArrayList<>(); -// bindAddresses.add("localhost"); -// -// List hostnames = new ArrayList<>(); -// hostnames.add("localhost"); -// -// Set endpointConfigurations = new LinkedHashSet<>(); -// -// for (String bindAddress : bindAddresses) { -// for (String hostname : hostnames) { -// EndpointConfiguration.Builder base = EndpointConfiguration.newBuilder() -// .setBindAddress(bindAddress) -// .setHostname(hostname) -// .setPath("/test") -// .setCertificate(serverCertificate) -// .addTokenPolicies(USER_TOKEN_POLICY_ANONYMOUS); -// -// // TCP Transport Endpoints -// endpointConfigurations.add( -// base.copy() -// .setBindPort(tcpBindPort) -// .setSecurityPolicy(SecurityPolicy.None) -// .setSecurityMode(MessageSecurityMode.None) -// .setTransportProfile(TransportProfile.TCP_UASC_UABINARY) -// .build() -// ); -// -// endpointConfigurations.add( -// base.copy() -// .setBindPort(tcpBindPort) -// .setSecurityPolicy(SecurityPolicy.Basic256Sha256) -// .setSecurityMode(MessageSecurityMode.SignAndEncrypt) -// .setTransportProfile(TransportProfile.TCP_UASC_UABINARY) -// .build() -// ); -// } -// } -// -// UaStackServerConfig serverConfig = configureServer( -// UaStackServerConfig.builder() -// .setEndpoints(endpointConfigurations) -// .setCertificateManager(serverCertificateManager) -// .setCertificateValidator(serverCertificateValidator) -// ).build(); -// -// stackServer = new UaStackServer(serverConfig); -// stackServer.startup().get(); -// -// String discoveryUrl = getDiscoveryUrl(); -// -// EndpointDescription endpoint = selectEndpoint( -// DiscoveryClient.getEndpoints(discoveryUrl) -// .thenApply(endpoints -> { -// endpoints.forEach(e -> -// logger.info("discovered endpoint: {}", e.getEndpointUrl())); -// -// return endpoints; -// }) -// .get() -// ); -// -// UaStackClientConfig clientConfig = configureClient( -// UaStackClientConfig.builder() -// .setEndpoint(endpoint) -// .setKeyPair(clientKeyPair) -// .setCertificate(clientCertificate) -// .setRequestTimeout(uint(5000)) -// ).build(); -// -// stackClient = UaStackClient.create(clientConfig); -// stackClient.connect().get(); -// } -// -// @AfterSuite -// public void tearDownClientServer() throws Exception { -// stackClient.disconnect().get(); -// stackServer.shutdown().get(); -// } -// -// protected EndpointDescription selectEndpoint(List endpoints) { -// return endpoints.get(0); -// } -// -// protected UaStackClientConfigBuilder configureClient(UaStackClientConfigBuilder builder) { -// return builder; -// } -// -// protected UaStackServerConfigBuilder configureServer(UaStackServerConfigBuilder builder) { -// return builder; -// } -// -// protected int getTcpBindPort() { -// return tcpBindPort; -// } -// -// protected String getDiscoveryUrl() { -// return String.format("opc.tcp://localhost:%d/test", getTcpBindPort()); -// } -// -// public static class TestTcpStackIntegrationTest extends StackIntegrationTest { -// -// @Test -// public void test() { -// } -// -// } -// -// } diff --git a/opc-ua-stack/transport/src/test/java/org/eclipse/milo/opcua/stack/transport/client/tcp/OpcTcpTransportTest.java b/opc-ua-stack/transport/src/test/java/org/eclipse/milo/opcua/stack/transport/client/tcp/OpcTcpTransportTest.java index bcdce81a30..da52f6fe1f 100644 --- a/opc-ua-stack/transport/src/test/java/org/eclipse/milo/opcua/stack/transport/client/tcp/OpcTcpTransportTest.java +++ b/opc-ua-stack/transport/src/test/java/org/eclipse/milo/opcua/stack/transport/client/tcp/OpcTcpTransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -16,7 +16,6 @@ import java.net.InetSocketAddress; import java.security.KeyPair; -import java.security.Security; import java.security.cert.X509Certificate; import java.util.List; import java.util.Objects; @@ -25,7 +24,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.channel.messages.ErrorMessage; import org.eclipse.milo.opcua.stack.core.encoding.DefaultEncodingContext; @@ -66,11 +64,6 @@ class OpcTcpTransportTest extends SecurityFixture { private static final Logger LOGGER = LoggerFactory.getLogger(OpcTcpTransportTest.class); - static { - // Required for SecurityPolicy.Aes256_Sha256_RsaPss - Security.addProvider(new BouncyCastleProvider()); - } - private static Stream provideSecurityParameters() { return Stream.of( Arguments.of(SecurityPolicy.None, MessageSecurityMode.None), From 6909d4735d3c2907c5af87862bf478dfa06a2be0 Mon Sep 17 00:00:00 2001 From: Kevin Herron Date: Tue, 4 Nov 2025 12:49:10 -0800 Subject: [PATCH 2/2] Add security policy integration tests - Add a parameterized test covering all SecurityPolicy/MessageSecurityMode combinations - Refactor KeyStoreLoader to support both server and client keystores - Update TestServer to generate endpoints for all security configurations --- opc-ua-sdk/integration-tests/pom.xml | 6 + .../core/OpcUaClientServerSecurityTest.java | 150 ++++++++++ .../milo/opcua/sdk/test/KeyStoreLoader.java | 265 ++++++++++++++---- .../milo/opcua/sdk/test/TestServer.java | 52 ++-- 4 files changed, 408 insertions(+), 65 deletions(-) create mode 100644 opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/core/OpcUaClientServerSecurityTest.java diff --git a/opc-ua-sdk/integration-tests/pom.xml b/opc-ua-sdk/integration-tests/pom.xml index d6cc7eb9fc..c24da0e2ad 100644 --- a/opc-ua-sdk/integration-tests/pom.xml +++ b/opc-ua-sdk/integration-tests/pom.xml @@ -77,6 +77,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + diff --git a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/core/OpcUaClientServerSecurityTest.java b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/core/OpcUaClientServerSecurityTest.java new file mode 100644 index 0000000000..a5054f5865 --- /dev/null +++ b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/core/OpcUaClientServerSecurityTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025 the Eclipse Milo Authors + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.milo.opcua.sdk.core; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.File; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.eclipse.milo.opcua.sdk.client.OpcUaClient; +import org.eclipse.milo.opcua.sdk.client.OpcUaClientConfigBuilder; +import org.eclipse.milo.opcua.sdk.server.OpcUaServer; +import org.eclipse.milo.opcua.sdk.test.KeyStoreLoader; +import org.eclipse.milo.opcua.sdk.test.TestServer; +import org.eclipse.milo.opcua.stack.core.security.DefaultClientCertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.MemoryCertificateQuarantine; +import org.eclipse.milo.opcua.stack.core.security.MemoryTrustListManager; +import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; +import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; +import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class OpcUaClientServerSecurityTest { + + private OpcUaServer server; + private KeyPair clientKeyPair; + private X509Certificate clientCertificate; + private X509Certificate[] clientCertificateChain; + private MemoryTrustListManager trustListManager; + + @BeforeEach + public void setup() throws Exception { + TestServer testServer = TestServer.create(); + server = testServer.getServer(); + server.startup().get(); + + File securityTempDir = new File(System.getProperty("java.io.tmpdir"), "security"); + KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir); + + clientKeyPair = loader.getClientKeyPair(); + clientCertificate = loader.getClientCertificate(); + clientCertificateChain = loader.getClientCertificateChain(); + + trustListManager = new MemoryTrustListManager(); + trustListManager.addTrustedCertificate(loader.getServerCertificate()); + + server + .getConfig() + .getCertificateManager() + .getCertificateGroups() + .forEach(group -> group.getTrustListManager().addTrustedCertificate(clientCertificate)); + } + + @AfterEach + public void tearDown() throws Exception { + server.shutdown().get(2, TimeUnit.SECONDS); + } + + @ParameterizedTest + @MethodSource("securityConfigurations") + public void testClientConnectsWithSecurityConfiguration( + SecurityPolicy securityPolicy, MessageSecurityMode messageSecurityMode) { + + assertDoesNotThrow( + () -> { + OpcUaClient client = + OpcUaClient.create( + server.getConfig().getEndpoints().iterator().next().getEndpointUrl(), + endpoints -> + endpoints.stream() + .filter( + e -> + Objects.equals(e.getSecurityPolicyUri(), securityPolicy.getUri()) + && Objects.equals(e.getSecurityMode(), messageSecurityMode)) + .findFirst(), + transportConfigBuilder -> {}, + this::configureClient); + + try { + client.connect(); + + assertDoesNotThrow(client::getSession); + } finally { + client.disconnect(); + } + }, + String.format( + "Failed to connect with SecurityPolicy=%s and MessageSecurityMode=%s", + securityPolicy, messageSecurityMode)); + } + + private void configureClient(OpcUaClientConfigBuilder configBuilder) { + var certificateValidator = + new DefaultClientCertificateValidator(trustListManager, new MemoryCertificateQuarantine()); + + String applicationUri = + CertificateUtil.getSanUri(clientCertificate) + .orElse("urn:eclipse:milo:test:security:client"); + + configBuilder + .setApplicationName(LocalizedText.english("eclipse milo security test client")) + .setApplicationUri(applicationUri) + .setKeyPair(clientKeyPair) + .setCertificate(clientCertificate) + .setCertificateChain(clientCertificateChain) + .setCertificateValidator(certificateValidator); + } + + private static Stream securityConfigurations() { + return Stream.of( + // SecurityPolicy.None with MessageSecurityMode.None + Arguments.of(SecurityPolicy.None, MessageSecurityMode.None), + + // Basic128Rsa15 with Sign and SignAndEncrypt + Arguments.of(SecurityPolicy.Basic128Rsa15, MessageSecurityMode.Sign), + Arguments.of(SecurityPolicy.Basic128Rsa15, MessageSecurityMode.SignAndEncrypt), + + // Basic256 with Sign and SignAndEncrypt + Arguments.of(SecurityPolicy.Basic256, MessageSecurityMode.Sign), + Arguments.of(SecurityPolicy.Basic256, MessageSecurityMode.SignAndEncrypt), + + // Basic256Sha256 with Sign and SignAndEncrypt + Arguments.of(SecurityPolicy.Basic256Sha256, MessageSecurityMode.Sign), + Arguments.of(SecurityPolicy.Basic256Sha256, MessageSecurityMode.SignAndEncrypt), + + // Aes128_Sha256_RsaOaep with Sign and SignAndEncrypt + Arguments.of(SecurityPolicy.Aes128_Sha256_RsaOaep, MessageSecurityMode.Sign), + Arguments.of(SecurityPolicy.Aes128_Sha256_RsaOaep, MessageSecurityMode.SignAndEncrypt), + + // Aes256_Sha256_RsaPss with Sign and SignAndEncrypt + Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurityMode.Sign), + Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurityMode.SignAndEncrypt)); + } +} diff --git a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/KeyStoreLoader.java b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/KeyStoreLoader.java index 2da81b06ed..ef37966775 100644 --- a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/KeyStoreLoader.java +++ b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/KeyStoreLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the Eclipse Milo Authors + * Copyright (c) 2025 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -17,7 +17,6 @@ import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashSet; @@ -29,12 +28,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class KeyStoreLoader { +public class KeyStoreLoader { private static final Pattern IP_ADDR_PATTERN = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); private static final String SERVER_ALIAS = "server-ai"; + private static final String CLIENT_ALIAS = "client-ai"; private static final char[] PASSWORD = "password".toCharArray(); private static final Logger LOGGER = LoggerFactory.getLogger(KeyStoreLoader.class); @@ -43,77 +43,248 @@ class KeyStoreLoader { private X509Certificate serverCertificate; private KeyPair serverKeyPair; - KeyStoreLoader load(File baseDir) throws Exception { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); + private X509Certificate[] clientCertificateChain; + private X509Certificate clientCertificate; + private KeyPair clientKeyPair; - File serverKeyStore = baseDir.toPath().resolve("example-server.pfx").toFile(); + /** + * Loads or creates server and client keystores with self-signed certificates. + * + *

If keystores do not exist at the expected paths, they will be created with new self-signed + * certificates. Server certificates include hostname and IP address SANs. + * + * @param baseDir the directory containing or where keystores will be created. + * @return this KeyStoreLoader instance for method chaining. + * @throws IllegalArgumentException if baseDir is null. + * @throws Exception if keystore operations fail. + */ + public KeyStoreLoader load(File baseDir) throws Exception { + if (baseDir == null) { + throw new IllegalArgumentException("baseDir cannot be null"); + } - LOGGER.debug("Loading KeyStore at {}", serverKeyStore); + KeyStore serverKeyStore = + loadOrCreateKeyStore( + baseDir.toPath().resolve("test-server.pfx").toFile(), + SERVER_ALIAS, + "Eclipse Milo Test Server", + "urn:eclipse:milo:test:server:" + UUID.randomUUID(), + true); - if (!serverKeyStore.exists()) { - keyStore.load(null, PASSWORD); + loadServerCredentials(serverKeyStore); - KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048); + KeyStore clientKeyStore = + loadOrCreateKeyStore( + baseDir.toPath().resolve("test-client.pfx").toFile(), + CLIENT_ALIAS, + "Eclipse Milo Test Client", + "urn:eclipse:milo:test:client:" + UUID.randomUUID(), + false); - String applicationUri = "urn:eclipse:milo:examples:server:" + UUID.randomUUID(); + loadClientCredentials(clientKeyStore); - SelfSignedCertificateBuilder builder = - new SelfSignedCertificateBuilder(keyPair) - .setCommonName("Eclipse Milo Example Server") - .setOrganization("digitalpetri") - .setOrganizationalUnit("dev") - .setLocalityName("Folsom") - .setStateName("CA") - .setCountryCode("US") - .setApplicationUri(applicationUri); + return this; + } - // Get as many hostnames and IP addresses as we can to list in the certificate. - var hostnames = new HashSet(); - hostnames.add(HostnameUtil.getHostname()); - hostnames.addAll(HostnameUtil.getHostnames("0.0.0.0", false)); + /** + * Loads an existing PKCS12 keystore or creates a new one if it doesn't exist. + * + * @param keyStoreFile the keystore file to load or create. + * @param alias the alias for the key entry. + * @param commonName the common name for the certificate. + * @param applicationUri the OPC UA application URI. + * @param includeHostnames whether to include hostname and IP address SANs. + * @return the loaded or created KeyStore. + * @throws Exception if keystore operations fail. + */ + private KeyStore loadOrCreateKeyStore( + File keyStoreFile, + String alias, + String commonName, + String applicationUri, + boolean includeHostnames) + throws Exception { - for (String hostname : hostnames) { - if (IP_ADDR_PATTERN.matcher(hostname).matches()) { - builder.addIpAddress(hostname); - } else { - builder.addDnsName(hostname); - } - } + KeyStore keyStore = KeyStore.getInstance("PKCS12"); - X509Certificate certificate = builder.build(); + LOGGER.debug("Loading KeyStore at {}", keyStoreFile); - keyStore.setKeyEntry( - SERVER_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[] {certificate}); - keyStore.store(new FileOutputStream(serverKeyStore), PASSWORD); + if (!keyStoreFile.exists()) { + createNewKeyStore( + keyStore, keyStoreFile, alias, commonName, applicationUri, includeHostnames); } else { - keyStore.load(new FileInputStream(serverKeyStore), PASSWORD); + try (FileInputStream fis = new FileInputStream(keyStoreFile)) { + keyStore.load(fis, PASSWORD); + } + } + + return keyStore; + } + + /** + * Creates a new PKCS12 keystore with a self-signed certificate. + * + * @param keyStore the empty KeyStore instance to populate. + * @param keyStoreFile the file where the keystore will be saved. + * @param alias the alias for the key entry. + * @param commonName the common name for the certificate. + * @param applicationUri the OPC UA application URI. + * @param includeHostnames whether to include hostname and IP address SANs. + * @throws Exception if keystore creation or storage fails. + */ + private void createNewKeyStore( + KeyStore keyStore, + File keyStoreFile, + String alias, + String commonName, + String applicationUri, + boolean includeHostnames) + throws Exception { + + keyStore.load(null, PASSWORD); + + KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048); + + SelfSignedCertificateBuilder builder = + new SelfSignedCertificateBuilder(keyPair) + .setCommonName(commonName) + .setOrganization("digitalpetri") + .setOrganizationalUnit("dev") + .setLocalityName("Folsom") + .setStateName("CA") + .setCountryCode("US") + .setApplicationUri(applicationUri); + + if (includeHostnames) { + addHostnamesToCertificate(builder); } - Key serverPrivateKey = keyStore.getKey(SERVER_ALIAS, PASSWORD); - if (serverPrivateKey instanceof PrivateKey) { + X509Certificate certificate = builder.build(); + + keyStore.setKeyEntry( + alias, keyPair.getPrivate(), PASSWORD, new X509Certificate[] {certificate}); + + try (FileOutputStream fos = new FileOutputStream(keyStoreFile)) { + keyStore.store(fos, PASSWORD); + } + } + + /** + * Loads server certificate, certificate chain, and key pair from the keystore. + * + * @param keyStore the keystore containing server credentials. + * @throws Exception if credential extraction fails. + */ + private void loadServerCredentials(KeyStore keyStore) throws Exception { + Key privateKey = keyStore.getKey(SERVER_ALIAS, PASSWORD); + if (privateKey instanceof PrivateKey) { serverCertificate = (X509Certificate) keyStore.getCertificate(SERVER_ALIAS); + serverCertificateChain = getCertificateChain(keyStore, SERVER_ALIAS); + serverKeyPair = new KeyPair(serverCertificate.getPublicKey(), (PrivateKey) privateKey); + } + } - serverCertificateChain = - Arrays.stream(keyStore.getCertificateChain(SERVER_ALIAS)) - .map(X509Certificate.class::cast) - .toArray(X509Certificate[]::new); + /** + * Loads client certificate, certificate chain, and key pair from the keystore. + * + * @param keyStore the keystore containing client credentials. + * @throws Exception if credential extraction fails. + */ + private void loadClientCredentials(KeyStore keyStore) throws Exception { + Key privateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD); + if (privateKey instanceof PrivateKey) { + clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS); + clientCertificateChain = getCertificateChain(keyStore, CLIENT_ALIAS); + clientKeyPair = new KeyPair(clientCertificate.getPublicKey(), (PrivateKey) privateKey); + } + } + + /** + * Adds all available hostnames and IP addresses as SANs to the certificate. + * + * @param builder the certificate builder to configure. + */ + private static void addHostnamesToCertificate(SelfSignedCertificateBuilder builder) { + var hostnames = new HashSet(); + hostnames.add(HostnameUtil.getHostname()); + hostnames.addAll(HostnameUtil.getHostnames("0.0.0.0", false)); - PublicKey serverPublicKey = serverCertificate.getPublicKey(); - serverKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey); + for (String hostname : hostnames) { + if (IP_ADDR_PATTERN.matcher(hostname).matches()) { + builder.addIpAddress(hostname); + } else { + builder.addDnsName(hostname); + } } + } - return this; + /** + * Extracts the certificate chain for the given alias from the keystore. + * + * @param keyStore the keystore containing the certificate chain. + * @param alias the alias of the key entry. + * @return the certificate chain as an array of X509Certificate. + * @throws Exception if chain extraction fails. + */ + private static X509Certificate[] getCertificateChain(KeyStore keyStore, String alias) + throws Exception { + + return Arrays.stream(keyStore.getCertificateChain(alias)) + .map(X509Certificate.class::cast) + .toArray(X509Certificate[]::new); } - X509Certificate getServerCertificate() { + /** + * Returns the server certificate. + * + * @return the server X509 certificate. + */ + public X509Certificate getServerCertificate() { return serverCertificate; } + /** + * Returns the server certificate chain. + * + * @return the server certificate chain. + */ public X509Certificate[] getServerCertificateChain() { return serverCertificateChain; } - KeyPair getServerKeyPair() { + /** + * Returns the server key pair. + * + * @return the server key pair. + */ + public KeyPair getServerKeyPair() { return serverKeyPair; } + + /** + * Returns the client certificate. + * + * @return the client X509 certificate. + */ + public X509Certificate getClientCertificate() { + return clientCertificate; + } + + /** + * Returns the client certificate chain. + * + * @return the client certificate chain. + */ + public X509Certificate[] getClientCertificateChain() { + return clientCertificateChain; + } + + /** + * Returns the client key pair. + * + * @return the client key pair. + */ + public KeyPair getClientKeyPair() { + return clientKeyPair; + } } diff --git a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java index 4830b7c547..54c2216694 100644 --- a/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java +++ b/opc-ua-sdk/integration-tests/src/test/java/org/eclipse/milo/opcua/sdk/test/TestServer.java @@ -253,6 +253,19 @@ private static Set createEndpointConfigs(X509Certificate certifi Set hostnames = HostnameUtil.getHostnames("localhost", true); + List securityPolicies = + List.of( + SecurityPolicy.None, + SecurityPolicy.Basic128Rsa15, + SecurityPolicy.Basic256, + SecurityPolicy.Basic256Sha256, + SecurityPolicy.Aes128_Sha256_RsaOaep, + SecurityPolicy.Aes256_Sha256_RsaPss); + + List messageSecurityModes = + List.of( + MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt); + for (String bindAddress : bindAddresses) { for (String hostname : hostnames) { EndpointConfig.Builder builder = @@ -266,31 +279,34 @@ private static Set createEndpointConfigs(X509Certificate certifi USER_TOKEN_POLICY_USERNAME, USER_TOKEN_POLICY_X509); - EndpointConfig.Builder noSecurityBuilder = - builder - .copy() - .setSecurityPolicy(SecurityPolicy.None) - .setSecurityMode(MessageSecurityMode.None); - - endpointConfigurations.add(buildTcpEndpoint(port, noSecurityBuilder)); - - // TCP Basic256Sha256 / SignAndEncrypt - endpointConfigurations.add( - buildTcpEndpoint( - port, - builder - .copy() - .setSecurityPolicy(SecurityPolicy.Basic256Sha256) - .setSecurityMode(MessageSecurityMode.SignAndEncrypt))); + // Create endpoints for all SecurityPolicy/MessageSecurityMode combinations + // SecurityPolicy.None must only be combined with MessageSecurityMode.None + for (SecurityPolicy securityPolicy : securityPolicies) { + for (MessageSecurityMode messageMode : messageSecurityModes) { + // SecurityPolicy.None is only valid with MessageSecurityMode.None + if (securityPolicy == SecurityPolicy.None && messageMode != MessageSecurityMode.None) { + continue; + } + // MessageSecurityMode.None is only valid with SecurityPolicy.None + if (messageMode == MessageSecurityMode.None && securityPolicy != SecurityPolicy.None) { + continue; + } + + endpointConfigurations.add( + buildTcpEndpoint( + port, + builder.copy().setSecurityPolicy(securityPolicy).setSecurityMode(messageMode))); + } + } /* * It's good practice to provide a discovery-specific endpoint with no security. * It's required practice if all regular endpoints have security configured. * - * Usage of the "/discovery" suffix is defined by OPC UA Part 6: + * OPC UA Part 6 defines usage of the "/discovery" suffix: * * Each OPC UA Server Application implements the Discovery Service Set. If the OPC UA Server requires a - * different address for this Endpoint it shall create the address by appending the path "/discovery" to + * different address for this Endpoint, it shall create the address by appending the path "/discovery" to * its base address. */