This is an automated email from the ASF dual-hosted git repository. ldywicki pushed a commit to branch opcua-test-containers-v2 in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit a294faf580cd689cdcdc030d6e874e561b3cda50 Author: Łukasz Dywicki <[email protected]> AuthorDate: Tue Oct 22 13:01:17 2024 +0200 fix(plc4j): Use pre-provisioned security keys for OPC-UA tests. Signed-off-by: Łukasz Dywicki <[email protected]> --- .../apache/plc4x/java/opcua/KeystoreGenerator.java | 115 ++++++++++++++++ .../apache/plc4x/java/opcua/MiloTestContainer.java | 9 +- .../plc4x/java/opcua/OpcuaPlcDriverTest.java | 144 +++++++++++++-------- .../protocol/OpcuaSubscriptionHandleTest.java | 27 +--- .../milo/examples/server/TestMiloServer.java | 2 - .../opcua/src/test/resources/logback-test.xml | 4 +- 6 files changed, 223 insertions(+), 78 deletions(-) diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/KeystoreGenerator.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/KeystoreGenerator.java new file mode 100644 index 0000000000..c16f0b80e0 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/KeystoreGenerator.java @@ -0,0 +1,115 @@ +/* + * 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.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Set; +import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder; +import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator; + +/** + * Utility to generate server certificate - based on Eclipse Milo stuff. + */ +public class KeystoreGenerator { + + private final String password; + private final KeyStore keyStore; + private final X509Certificate certificate; + + public KeystoreGenerator(String password, int length, String applicationUri) { + this(password, length, applicationUri, "server-ai", "Milo Server"); + } + + public KeystoreGenerator(String password, int length, String applicationUri, String entryName, String commonName) { + this.password = password; + try { + this.keyStore = generate(password, length, applicationUri, entryName, commonName); + + Key serverPrivateKey = keyStore.getKey(entryName, password.toCharArray()); + if (serverPrivateKey instanceof PrivateKey) { + this.certificate = (X509Certificate) keyStore.getCertificate(entryName); + } else { + throw new IllegalStateException("Unexpected keystore entry, expected private key"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private KeyStore generate(String password, int length, String applicationUri, String entryName, String commonName) throws Exception { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, password.toCharArray()); + KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(length); + SelfSignedCertificateBuilder builder = (new SelfSignedCertificateBuilder( + keyPair)).setCommonName(commonName) + .setOrganization("Apache Software Foundation") + .setOrganizationalUnit("PLC4X") + .setLocalityName("PLC4J") + .setStateName("CA") + .setCountryCode("US") + .setApplicationUri(applicationUri); + + Set<String> hostnames = Set.of("127.0.0.1"); + + for (String hostname : hostnames) { + if (hostname.startsWith("\\d+\\.")) { + builder.addIpAddress(hostname); + } else { + builder.addDnsName(hostname); + } + } + + X509Certificate certificate = builder.build(); + keyStore.setKeyEntry(entryName, keyPair.getPrivate(), password.toCharArray(), new X509Certificate[]{ certificate }); + return keyStore; + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void writeKeyStoreTo(OutputStream stream) throws IOException, GeneralSecurityException { + keyStore.store(stream, password.toCharArray()); + stream.flush(); + } + + public void writeCertificateTo(OutputStream stream) throws IOException, CertificateEncodingException { + String data = "-----BEGIN CERTIFICATE-----\n" + + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(certificate.getEncoded()) + "\n" + + "-----END CERTIFICATE-----"; + + stream.write(data.getBytes()); + stream.flush(); + } + +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/MiloTestContainer.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/MiloTestContainer.java index db13a936ae..43989e2659 100644 --- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/MiloTestContainer.java +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/MiloTestContainer.java @@ -36,8 +36,15 @@ public class MiloTestContainer extends GenericContainer<MiloTestContainer> { public MiloTestContainer() { super(IMAGE); - waitingFor(Wait.forLogMessage("Server started\\s*", 1)); + waitingFor(Wait.forLogMessage("Server started\\s*", 1)) + // Uncomment below to debug Milo server + //.withStartupTimeout(Duration.ofMinutes(10)) + ; addExposedPort(12686); + + // Uncomment below to enable server debug + //withEnv("JAVA_TOOL_OPTIONS", "-agentlib:jdwp=transport=dt_socket,address=*:8000,server=y,suspend=y"); + //addFixedExposedPort(8000, 8000); } private static ImageFromDockerfile inlineImage() { 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 01432165a0..2e6bf163dd 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,6 +18,8 @@ */ package org.apache.plc4x.java.opcua; +import java.io.File; +import java.io.FileOutputStream; import java.lang.reflect.Array; import java.net.URLEncoder; import java.nio.charset.Charset; @@ -45,10 +47,8 @@ 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.apache.plc4x.test.DisableOnJenkinsFlag; 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.Arguments; @@ -59,15 +59,9 @@ import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.images.builder.ImageFromDockerfile; -import org.testcontainers.jib.JibImage; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -75,17 +69,23 @@ import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -@DisableOnJenkinsFlag @Testcontainers(disabledWithoutDocker = true) public class OpcuaPlcDriverTest { private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaPlcDriverTest.class); + private static final String APPLICATION_URI = "urn:org.apache:plc4x"; + private static final KeystoreGenerator SERVER_KEY_STORE_GENERATOR = new KeystoreGenerator("password", 2048, APPLICATION_URI); + private static final KeystoreGenerator CLIENT_KEY_STORE_GENERATOR = new KeystoreGenerator("changeit", 2048, APPLICATION_URI, "plc4x_plus_milo", "client"); + @Container - public final GenericContainer milo = new MiloTestContainer() + public final MiloTestContainer milo = new MiloTestContainer() //.withCreateContainerCmdModifier(cmd -> cmd.withHostName("test-opcua-server")) .withLogConsumer(new Slf4jLogConsumer(LOGGER)) - .withFileSystemBind("target/tmp/server/security", "/tmp/server/security", BindMode.READ_WRITE); + .withFileSystemBind(SECURITY_DIR.getAbsolutePath(), "/tmp/server/security") + //.withEnv("JAVA_TOOL_OPTIONS", "-agentlib:jdwp=transport=dt_socket,address=*:8000,server=y,suspend=y") +// .withCommand("java -cp '/opt/milo/*:/opt/milo/' org.eclipse.milo.examples.server.TestMiloServer") + ; // Read only variables of milo example server of version 3.6 private static final String BOOL_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Boolean"; @@ -142,8 +142,8 @@ public class OpcuaPlcDriverTest { //Tcp pattern of OPC UA private final String opcPattern = "opcua:tcp://"; - private final String paramSectionDivider = "?"; - private final String paramDivider = "&"; + private static final String PARAM_SECTION_DIVIDER = "?"; + private static final String PARAM_DIVIDER = "&"; private final String discoveryValidParamTrue = "discovery=true"; private final String discoveryValidParamFalse = "discovery=false"; @@ -155,35 +155,39 @@ public class OpcuaPlcDriverTest { final List<String> discoveryParamValidSet = List.of(discoveryValidParamTrue, discoveryValidParamFalse); List<String> discoveryParamCorruptedSet = List.of(discoveryCorruptedParamWrongValueNum, discoveryCorruptedParamWrongName); - - @BeforeEach - public void startUp() { - //System.out.println(milo.getMappedPort(12686)); - tcpConnectionAddress = String.format(opcPattern + miloLocalAddress, milo.getHost(), milo.getMappedPort(12686)) + "?endpoint-port=12686"; - connectionStringValidSet = List.of(tcpConnectionAddress); - } + private static File SECURITY_DIR; + private static File CLIENT_KEY_STORE; @BeforeAll - public static void setup() throws Exception { - // When switching JDK versions from a newer to an older version, - // this can cause the server to not start correctly. - // Deleting the directory makes sure the key-store is initialized correctly. -// Path securityBaseDir = Paths.get(System.getProperty("java.io.tmpdir"), "server", "security"); -// try { -// Files.delete(securityBaseDir); -// } catch (Exception e) { -// // Ignore this ... -// } -// -// exampleServer = new TestMiloServer(); -// exampleServer.startup().get(); + public static void prepare() throws Exception { + Path tempDirectory = Files.createTempDirectory("plc4x_opcua_client"); + + SECURITY_DIR = new File(tempDirectory.toFile().getAbsolutePath(), "server/security"); + File pkiDir = new File(SECURITY_DIR, "pki"); + File trustedCerts = new File(pkiDir, "trusted/certs"); + if (!pkiDir.mkdirs() || !trustedCerts.mkdirs()) { + throw new IllegalStateException("Could not start test - missing permissions to create temporary files"); + } + + // pre-provisioned server certificate + try (FileOutputStream bos = new FileOutputStream(new File(pkiDir.getParentFile(), "example-server.pfx"))) { + SERVER_KEY_STORE_GENERATOR.writeKeyStoreTo(bos); + } + + // pre-provisioned client certificate, doing it here because server might be slow in picking up them, and we don't want to wait with our tests + CLIENT_KEY_STORE = Files.createTempFile("plc4x_opcua_client_", ".p12").toAbsolutePath().toFile(); + try (FileOutputStream outputStream = new FileOutputStream(CLIENT_KEY_STORE)) { + CLIENT_KEY_STORE_GENERATOR.writeKeyStoreTo(outputStream); + } + try (FileOutputStream outputStream = new FileOutputStream(new File(trustedCerts, "plc4x.crt"))) { + CLIENT_KEY_STORE_GENERATOR.writeCertificateTo(outputStream); + } } - @AfterAll - public static void tearDown() throws Exception { -// if (exampleServer != null) { -// exampleServer.shutdown().get(); -// } + @BeforeEach + public void startUp() throws Exception { + tcpConnectionAddress = String.format(opcPattern + miloLocalAddress, milo.getHost(), milo.getMappedPort(12686)) + "?endpoint-port=12686"; + connectionStringValidSet = List.of(tcpConnectionAddress); } @Nested @@ -276,7 +280,7 @@ public class OpcuaPlcDriverTest { return connectionStringValidSet.stream() .map(connectionAddress -> DynamicContainer.dynamicContainer(connectionAddress, () -> discoveryParamValidSet.stream().map(discoveryParam -> DynamicTest.dynamicTest(discoveryParam, () -> { - String connectionString = connectionAddress + paramDivider + discoveryParam; + String connectionString = connectionAddress + PARAM_DIVIDER + discoveryParam; PlcConnection opcuaConnection = new DefaultPlcDriverManager().getConnection(connectionString); Condition<PlcConnection> is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); assertThat(opcuaConnection).is(is_connected); @@ -340,6 +344,39 @@ public class OpcuaPlcDriverTest { assertThat(response.getResponseCode("String")).isEqualTo(PlcResponseCode.OK); } } + + @Test + void staticConfig() throws Exception { + DefaultPlcDriverManager driverManager = new DefaultPlcDriverManager(); + + File certificateFile = Files.createTempFile("plc4x_opcua_server_", ".crt").toAbsolutePath().toFile(); + try (FileOutputStream outputStream = new FileOutputStream(certificateFile)) { + SERVER_KEY_STORE_GENERATOR.writeCertificateTo(outputStream); + } + + String options = params( + entry("discovery", "false"), + entry("server-certificate-file", certificateFile.toString().replace("\\", "/")), + entry("key-store-file", CLIENT_KEY_STORE.toString().replace("\\", "/")), // handle windows paths + entry("key-store-password", "changeit"), + entry("key-store-type", "pkcs12"), + entry("security-policy", SecurityPolicy.Basic256Sha256.name()), + entry("message-security", MessageSecurity.SIGN.name()) + ); + try (PlcConnection opcuaConnection = driverManager.getConnection(tcpConnectionAddress + PARAM_DIVIDER + + options)) { + Condition<PlcConnection> is_connected = new Condition<>(PlcConnection::isConnected, "is connected"); + assertThat(opcuaConnection).is(is_connected); + + PlcReadRequest.Builder builder = opcuaConnection.readRequestBuilder() + .addTagAddress("String", STRING_IDENTIFIER_READ_WRITE); + + PlcReadRequest request = builder.build(); + PlcReadResponse response = request.execute().get(); + + assertThat(response.getResponseCode("String")).isEqualTo(PlcResponseCode.OK); + } + } } @Nested @@ -533,7 +570,7 @@ public class OpcuaPlcDriverTest { assert !opcuaConnection.isConnected(); } - private String getConnectionString(SecurityPolicy policy, MessageSecurity messageSecurity) { + private String getConnectionString(SecurityPolicy policy, MessageSecurity messageSecurity) throws Exception { switch (policy) { case NONE: return tcpConnectionAddress; @@ -543,18 +580,15 @@ public class OpcuaPlcDriverTest { case Basic256Sha256: case Aes128_Sha256_RsaOaep: case Aes256_Sha256_RsaPss: - // this file and its contents should be populated by milo container - Path keyStoreFile = Paths.get("target/tmp/server/security/example-server.pfx"); - String connectionParams = Stream.of( - entry("key-store-file", keyStoreFile.toAbsolutePath().toString().replace("\\", "/")), // handle windows paths - entry("key-store-password", "password"), - entry("security-policy", policy.name()), - entry("message-security", messageSecurity.name()) - ) - .map(tuple -> tuple.getKey() + "=" + URLEncoder.encode(tuple.getValue(), Charset.defaultCharset())) - .collect(Collectors.joining(paramDivider)); - - return tcpConnectionAddress + paramDivider + connectionParams; + String connectionParams = params( + entry("key-store-file", CLIENT_KEY_STORE.getAbsoluteFile().toString().replace("\\", "/")), // handle windows paths + entry("key-store-password", "changeit"), + entry("key-store-type", "pkcs12"), + entry("security-policy", policy.name()), + entry("message-security", messageSecurity.name()) + ); + + return tcpConnectionAddress + PARAM_DIVIDER + connectionParams; default: throw new IllegalStateException(); } @@ -582,4 +616,10 @@ public class OpcuaPlcDriverTest { Arguments.of(SecurityPolicy.Aes256_Sha256_RsaPss, MessageSecurity.SIGN_ENCRYPT) ); } + + private static String params(Entry<String, String> ... entries) { + return Stream.of(entries) + .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), Charset.defaultCharset())) + .collect(Collectors.joining(PARAM_DIVIDER)); + } } 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 094e09bb3b..37dec0d08f 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,6 +18,7 @@ */ package org.apache.plc4x.java.opcua.protocol; +import java.io.ByteArrayOutputStream; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; import org.apache.plc4x.java.DefaultPlcDriverManager; @@ -27,8 +28,7 @@ import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse; import org.apache.plc4x.java.api.types.PlcResponseCode; import org.apache.plc4x.java.opcua.MiloTestContainer; import org.apache.plc4x.java.opcua.OpcuaPlcDriverTest; -import org.apache.plc4x.test.DisableOnJenkinsFlag; -import org.apache.plc4x.test.DisableOnParallelsVmFlag; +import org.apache.plc4x.java.opcua.KeystoreGenerator; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -36,13 +36,9 @@ import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.concurrent.TimeUnit; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -56,18 +52,14 @@ import static org.junit.jupiter.api.Assertions.fail; // cdutz: I have done way more than my fair share on tracking down this issue and am simply giving up on it. // I tracked it down into the core of Milo several times now, but got lost in there. // It's not a big issue as the GitHub runners and the Apache Jenkins still run the test. -@DisableOnJenkinsFlag -@DisableOnParallelsVmFlag @Testcontainers(disabledWithoutDocker = true) public class OpcuaSubscriptionHandleTest { private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaPlcDriverTest.class); @Container - public final GenericContainer milo = new MiloTestContainer() - //.withCreateContainerCmdModifier(cmd -> cmd.withHostName("test-opcua-server")) - .withLogConsumer(new Slf4jLogConsumer(LOGGER)) - .withFileSystemBind("target/tmp/server/security", "/tmp/server/security", BindMode.READ_WRITE); + public final MiloTestContainer milo = new MiloTestContainer() + .withLogConsumer(new Slf4jLogConsumer(LOGGER)); // Address of local milo server private static final String miloLocalAddress = "%s:%d/milo"; @@ -102,16 +94,7 @@ public class OpcuaSubscriptionHandleTest { // When switching JDK versions from a newer to an older version, // this can cause the server to not start correctly. // Deleting the directory makes sure the key-store is initialized correctly. - String tcpConnectionAddress = String.format(opcPattern + miloLocalAddress, milo.getHost(), milo.getMappedPort(12686)) + "?endpoint-port=12686"; - - Path securityBaseDir = Paths.get(System.getProperty("java.io.tmpdir"), "server", "security"); - try { - Files.delete(securityBaseDir); - } catch (Exception e) { - // Ignore this ... - } - //Connect opcuaConnection = new DefaultPlcDriverManager().getConnection(tcpConnectionAddress); assertThat(opcuaConnection).extracting(PlcConnection::isConnected).isEqualTo(true); 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 872afbfb0f..d886afb11c 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 @@ -123,8 +123,6 @@ public class TestMiloServer { ); DefaultTrustListManager trustListManager = new DefaultTrustListManager(pkiDir); - trustListManager.setTrustedCertificates(new ArrayList<>(certificateManager.getCertificates())); - DefaultServerCertificateValidator certificateValidator = new DefaultServerCertificateValidator(trustListManager); diff --git a/plc4j/drivers/opcua/src/test/resources/logback-test.xml b/plc4j/drivers/opcua/src/test/resources/logback-test.xml index 695109a532..6e19a9fcfb 100644 --- a/plc4j/drivers/opcua/src/test/resources/logback-test.xml +++ b/plc4j/drivers/opcua/src/test/resources/logback-test.xml @@ -27,9 +27,11 @@ </encoder> </appender> - <root level="warn"> + <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> <logger name="org.apache.plc4x.java.spi.codegen" level="warn" /> + <logger name="com.github.dockerjava" level="WARN"/> + <logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/> </configuration>
