This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


The following commit(s) were added to refs/heads/develop by this push:
     new 7a5b599900 fix(plc4j/opcua): Generate a client certificate servers 
will accept
7a5b599900 is described below

commit 7a5b599900e31664f5062c961af05e32051434b6
Author: Christofer Dutz <[email protected]>
AuthorDate: Fri Aug 14 18:01:13 2026 +0200

    fix(plc4j/opcua): Generate a client certificate servers will accept
    
    When no key-store-file is configured the driver generates a self-signed
    application instance certificate, so this is the certificate most servers 
see on
    a first connection attempt. It had four problems, each enough on its own 
for a
    server with a modern security baseline to refuse the connection:
    
    - it was signed with SHA1withRSA, which is deprecated and inconsistent with 
the
      SHA-256 based security policies (reported in #2127)
    - the serial number was built from 40 random bytes, making it 40 octets 
long and
      negative about half the time; X.509 requires a positive serial of at most 
20
    - basicConstraints was set to true and keyUsage included keyCertSign, so the
      client announced itself as a certificate authority
    - there was no subjectKeyIdentifier, which servers use to look certificates 
up
    
    The certificate is now signed with SHA-256, carries a positive 16 octet 
serial,
    is marked as an end entity with the key usages OPC UA Part 6 lists for an
    application instance certificate, and has a subject key identifier.
    
    Two related repairs in the same method: it generated two 2048 bit key pairs 
that
    were never used, costing that work on every connect, and it asked for the 
"BC"
    provider by name, which only worked because another class had registered it
    first - a new CertificateGeneratorTest could not generate a certificate at 
all
    until that was made explicit.
    
    Fixes: #2127
---
 .../java/opcua/config/OpcuaConfiguration.java      |  10 ++
 .../java/opcua/context/CertificateGenerator.java   |  54 +++++++---
 .../java/opcua/context/OpcuaDriverContext.java     |   9 +-
 .../opcua/context/CertificateGeneratorTest.java    | 120 +++++++++++++++++++++
 .../modules/users/pages/protocols/opcua.adoc       |  15 +++
 website/asciidoc/modules/users/partials/opcua.adoc |   1 +
 6 files changed, 190 insertions(+), 19 deletions(-)

diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
index 2377068fd4..5e13086bad 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
@@ -31,6 +31,7 @@ import 
org.apache.plc4x.java.spi.config.annotations.ConfigurationParameter;
 import org.apache.plc4x.java.spi.config.annotations.Description;
 import 
org.apache.plc4x.java.spi.config.annotations.defaults.BooleanDefaultValue;
 import org.apache.plc4x.java.spi.config.annotations.defaults.LongDefaultValue;
+import org.apache.plc4x.java.spi.config.annotations.defaults.IntDefaultValue;
 import 
org.apache.plc4x.java.spi.config.annotations.defaults.StringDefaultValue;
 
 public class OpcuaConfiguration implements Configuration {
@@ -89,6 +90,11 @@ public class OpcuaConfiguration implements Configuration {
     @Description("Java keystore password used to access keystore and private 
key.")
     private String keyStorePassword;
 
+    @ConfigurationParameter("generated-key-size")
+    @IntDefaultValue(2048)
+    @Description("Size in bits of the RSA key of the certificate the driver 
generates when no `key-store-file` is configured. It is ignored when a key 
store is supplied, as the key then comes from that store. Some servers require 
a minimum size; 4096 is a common requirement.")
+    private int generatedKeySize;
+
     @ConfigurationParameter("server-certificate-file")
     @Description("Filesystem location where server certificate is located, 
supported formats are `DER` and `PEM`.")
     private String serverCertificateFile;
@@ -190,6 +196,10 @@ public class OpcuaConfiguration implements Configuration {
         return keyStoreType;
     }
 
+    public int getGeneratedKeySize() {
+        return generatedKeySize;
+    }
+
     public char[] getKeyStorePassword() {
         return keyStorePassword == null ? null : 
keyStorePassword.toCharArray();
     }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
index 5a18f7bede..ff771395ed 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
@@ -18,7 +18,6 @@
  */
 package org.apache.plc4x.java.opcua.context;
 
-import org.apache.commons.lang3.RandomUtils;
 import org.bouncycastle.asn1.DERSequence;
 import org.bouncycastle.asn1.x500.X500NameBuilder;
 import org.bouncycastle.asn1.x500.style.BCStyle;
@@ -48,18 +47,22 @@ public class CertificateGenerator<PKCS10CertificateRequest> 
{
     private static final Logger LOGGER = 
LoggerFactory.getLogger(CertificateGenerator.class);
     private static final String APPURI = "urn:eclipse:milo:plc4x:server";
 
+    /** Key size used when the connection string doesn't ask for a specific 
one. */
+    public static final int DEFAULT_KEY_SIZE = 2048;
+
+    /**
+     * Generates a self-signed application instance certificate with a 2048 
bit key.
+     */
     public static CertificateKeyPair generateCertificate() {
-        KeyPairGenerator kpg = null;
-        try {
-            kpg = KeyPairGenerator.getInstance("RSA");
-        } catch (NoSuchAlgorithmException e) {
-            LOGGER.error("Security Algorithim is unsupported for certificate");
-            return null;
-        }
-        kpg.initialize(2048);
-        KeyPair caKeys = kpg.generateKeyPair();
-        KeyPair userKeys = kpg.generateKeyPair();
+        return generateCertificate(DEFAULT_KEY_SIZE);
+    }
 
+    /**
+     * Generates a self-signed application instance certificate.
+     *
+     * @param keySize size of the RSA key in bits; servers may demand a 
minimum, commonly 4096
+     */
+    public static CertificateKeyPair generateCertificate(int keySize) {
         X500NameBuilder nameBuilder = new X500NameBuilder();
 
         nameBuilder.addRDN(BCStyle.CN, "Apache PLC4X Driver Client");
@@ -69,7 +72,10 @@ public class CertificateGenerator<PKCS10CertificateRequest> {
         nameBuilder.addRDN(BCStyle.ST, "DE");
         nameBuilder.addRDN(BCStyle.C, "US");
 
-        BigInteger serial = new BigInteger(RandomUtils.nextBytes(40));
+        // X.509 requires a positive serial number of at most 20 octets. 
Building it from 40 random
+        // bytes produced an over-long and, half the time, negative serial, 
which strict servers
+        // reject.
+        BigInteger serial = new BigInteger(127, new 
SecureRandom()).setBit(126);
 
         final Calendar calender = Calendar.getInstance();
         calender.add(Calendar.DATE, -1);
@@ -80,7 +86,7 @@ public class CertificateGenerator<PKCS10CertificateRequest> {
         KeyPairGenerator generator = null;
         try {
             generator = KeyPairGenerator.getInstance("RSA");
-            generator.initialize(2048, new SecureRandom());
+            generator.initialize(keySize, new SecureRandom());
             KeyPair keyPair = generator.generateKeyPair();
 
             SubjectPublicKeyInfo subjectPublicKeyInfo = 
SubjectPublicKeyInfo.getInstance(
@@ -99,15 +105,29 @@ public class 
CertificateGenerator<PKCS10CertificateRequest> {
 
             GeneralName[] gnArray = new GeneralName[]{new 
GeneralName(GeneralName.dNSName, InetAddress.getLocalHost().getHostName()), new 
GeneralName(GeneralName.uniformResourceIdentifier, APPURI)};
 
-            certificateBuilder.addExtension(Extension.authorityKeyIdentifier, 
false, new 
JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));
+            JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
+            certificateBuilder.addExtension(Extension.authorityKeyIdentifier, 
false, extensionUtils.createAuthorityKeyIdentifier(keyPair.getPublic()));
+            // Servers look the certificate up by its subject key identifier; 
without it some
+            // reject the certificate outright.
+            certificateBuilder.addExtension(Extension.subjectKeyIdentifier, 
false, extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
             certificateBuilder.addExtension(Extension.extendedKeyUsage, false, 
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, 
KeyPurposeId.id_kp_serverAuth}));
-            certificateBuilder.addExtension(Extension.keyUsage, false, new 
KeyUsage(KeyUsage.dataEncipherment | KeyUsage.digitalSignature | 
KeyUsage.keyAgreement | KeyUsage.keyCertSign | KeyUsage.keyEncipherment | 
KeyUsage.nonRepudiation));
-            certificateBuilder.addExtension(Extension.basicConstraints, false, 
new BasicConstraints(true));
+            // The key usages an application instance certificate is supposed 
to carry
+            // (OPC UA Part 6). keyCertSign has no business here - this 
certificate signs no other
+            // certificates.
+            certificateBuilder.addExtension(Extension.keyUsage, false, new 
KeyUsage(KeyUsage.dataEncipherment | KeyUsage.digitalSignature | 
KeyUsage.keyEncipherment | KeyUsage.nonRepudiation));
+            // This is an end entity certificate, not a CA. Claiming to be a 
CA made servers with
+            // stricter certificate checks refuse it.
+            certificateBuilder.addExtension(Extension.basicConstraints, false, 
new BasicConstraints(false));
 
             GeneralNames subjectAltNames = GeneralNames.getInstance(new 
DERSequence(gnArray));
             certificateBuilder.addExtension(Extension.subjectAlternativeName, 
false, subjectAltNames);
 
-            ContentSigner sigGen = new 
JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(keyPair.getPrivate());
+            // SHA-1 is deprecated and rejected by the SHA-256 based security 
policies as well as by
+            // any server holding a modern security baseline.
+            ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA")
+                // Pass the provider instance rather than the name "BC": this 
class would
+                // otherwise depend on some other class having registered it 
first.
+                .setProvider(new 
BouncyCastleProvider()).build(keyPair.getPrivate());
 
             X509CertificateHolder certificateHolder = 
certificateBuilder.build(sigGen);
 
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java
index b8533a38db..e8071d2865 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/OpcuaDriverContext.java
@@ -93,8 +93,13 @@ public class OpcuaDriverContext {
         String serverKeyStore = configuration.getKeyStoreFile();
 
         if (serverKeyStore == null) {
-            LOGGER.info("Client certificate not provided, creating temporary 
certificate and private key");
-            certificateKeyPair = CertificateGenerator.generateCertificate();
+            int keySize = configuration.getGeneratedKeySize();
+            if (keySize < 2048 || keySize % 1024 != 0) {
+                throw new IllegalArgumentException("'generated-key-size' has 
to be a multiple of 1024"
+                    + " and at least 2048, was " + keySize);
+            }
+            LOGGER.info("Client certificate not provided, creating temporary 
{} bit certificate and private key", keySize);
+            certificateKeyPair = 
CertificateGenerator.generateCertificate(keySize);
         } else {
             LOGGER.info("Loading KeyStore at {}", serverKeyStore);
 
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/CertificateGeneratorTest.java
 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/CertificateGeneratorTest.java
new file mode 100644
index 0000000000..216a797a0e
--- /dev/null
+++ 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/CertificateGeneratorTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.opcua.context;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The certificate the driver generates when no key store is configured is 
what most servers see on
+ * a first connection attempt, so it has to satisfy the checks a server 
performs. It used to be
+ * signed with SHA-1, flagged as a CA and carried an over-long, often negative 
serial number, which
+ * servers with a modern security baseline reject (see GH-2127).
+ */
+class CertificateGeneratorTest {
+
+    private static X509Certificate certificate;
+
+    @BeforeAll
+    static void generate() {
+        CertificateKeyPair keyPair = 
CertificateGenerator.generateCertificate();
+        assertNotNull(keyPair, "certificate generation failed");
+        certificate = keyPair.getCertificate();
+    }
+
+    @Test
+    void isSignedWithSha256() {
+        assertEquals("SHA256WITHRSA", 
certificate.getSigAlgName().toUpperCase(),
+            "SHA-1 signatures are rejected by the SHA-256 based security 
policies");
+    }
+
+    @Test
+    void hasAUsableKey() {
+        assertInstanceOfRsa();
+        assertEquals(CertificateGenerator.DEFAULT_KEY_SIZE,
+            ((RSAPublicKey) 
certificate.getPublicKey()).getModulus().bitLength());
+    }
+
+    /**
+     * Servers can demand a larger key than the default; 'generated-key-size' 
asks for one.
+     */
+    @Test
+    void honoursTheRequestedKeySize() {
+        CertificateKeyPair keyPair = 
CertificateGenerator.generateCertificate(4096);
+
+        assertNotNull(keyPair);
+        assertEquals(4096, ((RSAPublicKey) 
keyPair.getCertificate().getPublicKey()).getModulus().bitLength());
+        assertEquals("SHA256WITHRSA", 
keyPair.getCertificate().getSigAlgName().toUpperCase());
+    }
+
+    /**
+     * X.509 requires a positive serial number of at most 20 octets.
+     */
+    @Test
+    void hasASpecConformingSerialNumber() {
+        BigInteger serial = certificate.getSerialNumber();
+
+        assertEquals(1, serial.signum(), "the serial number has to be 
positive");
+        assertTrue(serial.toByteArray().length <= 20,
+            "the serial number must not exceed 20 octets, was " + 
serial.toByteArray().length);
+    }
+
+    /**
+     * This is an application instance certificate; it signs nothing but 
itself.
+     */
+    @Test
+    void isNotMarkedAsACertificateAuthority() {
+        assertEquals(-1, certificate.getBasicConstraints(), "the certificate 
must not claim to be a CA");
+
+        boolean[] keyUsage = certificate.getKeyUsage();
+        assertNotNull(keyUsage);
+        assertFalse(keyUsage[5], "keyCertSign has no business on an 
application instance certificate");
+        assertTrue(keyUsage[0], "digitalSignature is required");
+        assertTrue(keyUsage[2], "keyEncipherment is required");
+        assertTrue(keyUsage[3], "dataEncipherment is required");
+    }
+
+    /**
+     * Servers look certificates up by their subject key identifier.
+     */
+    @Test
+    void carriesASubjectKeyIdentifier() {
+        assertNotNull(certificate.getExtensionValue("2.5.29.14"), "subject key 
identifier is missing");
+    }
+
+    @Test
+    void carriesTheApplicationUriAsSubjectAlternativeName() throws Exception {
+        assertNotNull(certificate.getSubjectAlternativeNames(),
+            "the application URI has to be present as a subject alternative 
name");
+    }
+
+    private void assertInstanceOfRsa() {
+        assertTrue(certificate.getPublicKey() instanceof RSAPublicKey,
+            "expected an RSA key, got " + 
certificate.getPublicKey().getAlgorithm());
+    }
+}
diff --git a/website/asciidoc/modules/users/pages/protocols/opcua.adoc 
b/website/asciidoc/modules/users/pages/protocols/opcua.adoc
index 0d20f0aad4..2af6efbe10 100644
--- a/website/asciidoc/modules/users/pages/protocols/opcua.adoc
+++ b/website/asciidoc/modules/users/pages/protocols/opcua.adoc
@@ -92,6 +92,21 @@ WARNING: `insecure-certificate-verification=true` disables 
server certificate ve
 altogether. This makes the connection vulnerable to man-in-the-middle attacks 
and is intended for
 local testing only.
 
+=== Client certificate
+
+If no `key-store-file` is configured, the driver generates a self-signed 
application instance
+certificate for the session. It is a 2048 bit RSA key signed with SHA-256; use 
`generated-key-size`
+to ask for a larger key when the server demands one:
+
+----
+opcua:tcp://127.0.0.1:12686?security-policy=Basic256Sha256&message-security=SIGN_ENCRYPT&generated-key-size=4096
+----
+
+A generated certificate is fresh for every connection, so a server that keeps 
a trust list will
+reject it until it is trusted there. For anything beyond a first connection 
attempt, supply your own
+certificate through `key-store-file` instead - see
+link:../getting-started/opcua-client-certificate.html[the client certificate 
tutorial].
+
 === Negotiation procedure
 Depending on settings driver might or might not attempt to discover endpoints 
from remote server.
 In case when `discovery` option is set to `true` driver will look up server 
certificate through connection attempt.
diff --git a/website/asciidoc/modules/users/partials/opcua.adoc 
b/website/asciidoc/modules/users/partials/opcua.adoc
index 7b080a2056..036520b75e 100644
--- a/website/asciidoc/modules/users/partials/opcua.adoc
+++ b/website/asciidoc/modules/users/partials/opcua.adoc
@@ -56,6 +56,7 @@ This option is effective only when `securityPolicy` turns 
encryption (anything b
 |`key-store-type` |STRING |pkcs12| |Keystore type used to access keystore and 
private key, defaults to PKCS (for Java 11+). +
 Possible values are between others `jks`, `pkcs11`, `dks`, `jceks`.
 |`key-store-password` |STRING | | |Java keystore password used to access 
keystore and private key.
+|`generated-key-size` |INT |2048| |Size in bits of the RSA key of the 
certificate the driver generates when no `key-store-file` is configured. It is 
ignored when a key store is supplied, as the key then comes from that store. 
Some servers require a minimum size; 4096 is a common requirement.
 |`server-certificate-file` |STRING | | |Filesystem location where server 
certificate is located, supported formats are `DER` and `PEM`.
 |`trust-store-file` |STRING | | |The trust store file used to verify server 
certificates and its chain.
 |`trust-store-type` |STRING |pkcs12| |Keystore type used to access keystore 
and private key, defaults to PKCS (for Java 11+). +

Reply via email to