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+). +