From 99d15d914e5bdb5ad4dc95491b90cd50a622ceb7 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 14 Oct 2026 12:08:35 +0200 Subject: [PATCH 1/5] CAMEL-25645: camel-milo - let the OPC UA client validate the server certificate against a trust list milo-client and milo-browse kept the default certificate validator of the Eclipse Milo client configuration, which accepts any server certificate, and had no option to configure trust material. The client now validates the server certificate whenever the selected endpoint uses a security policy other than None, with the trust list in trustListDirectory or the validator in certificateValidator, and does not connect when neither is configured. The new trustAllCertificates option, marked insecure:ssl, accepts any server certificate. The same validator checks the certificate that the user identity token is encrypted with. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Andrea Cosentino --- .../camel/catalog/components/milo-browse.json | 14 +- .../catalog/docs/milo-browse-component.adoc | 2 +- .../catalog/docs/milo-client-component.adoc | 37 ++- .../browse/MiloBrowseComponentConfigurer.java | 18 ++ .../browse/MiloBrowseEndpointConfigurer.java | 18 ++ .../browse/MiloBrowseEndpointUriFactory.java | 5 +- .../client/MiloClientComponentConfigurer.java | 18 ++ .../client/MiloClientEndpointConfigurer.java | 18 ++ .../component/milo/browse/milo-browse.json | 14 +- .../src/main/docs/milo-browse-component.adoc | 2 +- .../src/main/docs/milo-client-component.adoc | 37 ++- .../milo/client/MiloClientConfiguration.java | 73 ++++- .../internal/ServerCertificateTrust.java | 163 +++++++++++ .../client/internal/SubscriptionManager.java | 19 +- .../milo/ServerCertificateValidationTest.java | 189 ++++++++++++ .../client/MiloClientConfigurationTest.java | 83 ++++++ .../internal/ServerCertificateTrustTest.java | 162 +++++++++++ .../MiloBrowseComponentBuilderFactory.java | 63 ++++ .../MiloClientComponentBuilderFactory.java | 63 ++++ .../dsl/MiloBrowseEndpointBuilderFactory.java | 91 ++++++ .../dsl/MiloClientEndpointBuilderFactory.java | 273 ++++++++++++++++++ 21 files changed, 1345 insertions(+), 17 deletions(-) create mode 100644 components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java create mode 100644 components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java create mode 100644 components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java create mode 100644 components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json index 9bca6522e6575..b22c573e0c05b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json @@ -49,8 +49,11 @@ "requestTimeout": { "index": 22, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 23, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 24, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 25, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 26, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The node ids.", "constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" } @@ -87,7 +90,10 @@ "requestTimeout": { "index": 28, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 29, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 30, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 31, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 32, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-browse-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-browse-component.adoc index 3356e17c1e68a..a6d6a530733d5 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-browse-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-browse-component.adoc @@ -54,7 +54,7 @@ include::partial$component-endpoint-headers.adoc[] === Client The browse component shares the same base options like the Camel Milo Client component, e.g. concerning topics like discovery, -security policies, the construction of node ids, etc. +security policies, the validation of the server certificate, the construction of node ids, etc. Please refer to the documentation of the Camel Milo Client component for further details. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc index 90ff4f0cb343f..9527e781cc490 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc @@ -203,10 +203,45 @@ The known security policy URIs and enum literals are can be seen here: https://g NOTE: In any case security policies are considered case sensitive. +When `allowedSecurityPolicies` is not set, the client accepts every security policy the server offers and uses the +endpoint with the highest security level reported by the server, which can be `None`. To make sure the connection is +secured, set `allowedSecurityPolicies` to the secured policies you expect the server to offer. + +=== Validating the server certificate + +When the selected endpoint uses a security policy other than `None`, the client validates the certificate of the +server when it opens the secure channel and the session. Configure the trusted certificates with one of these options: + +* `trustListDirectory`: a directory holding the trust list, in the layout used by Eclipse Milo. Put the certificate of +the server, or the certificate of the CA that issued it, in `trusted/certs`. Certificates of intermediate CAs that are +only used to build the chain go in `issuer/certs`, and certificate revocation lists in `trusted/crl` and `issuer/crl`. +The directories are created if they do not exist. A server certificate that fails validation is written to +`rejected/certs`, from where it can be moved to `trusted/certs` once it has been checked. +* `certificateValidator`: a custom `org.eclipse.milo.opcua.stack.core.security.CertificateValidator`. It is used +instead of `trustListDirectory` when both are set. + +The trust list checks that the certificate chains up to a trusted certificate. The optional checks of Eclipse Milo, +such as the validity period, the host name or the application URI, are only logged. To enforce them, configure a +`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the checks you need. + +If neither option is configured, the client does not connect to an endpoint that uses a security policy other than +`None`. Setting `trustAllCertificates` to `true` makes the client accept any server certificate instead. This is +insecure: the connection is encrypted, but the client does not authenticate the server it talks to. + +The same validator checks the server certificate that the user name and password are encrypted with. With the +security policy `None`, the server is not authenticated, whatever the configuration. + +A secured policy also requires a client certificate and key, configured with `keyStoreUrl` and the related options. +For example: + +---- +milo-client:opc.tcp://localhost:4840?node=RAW(ns=2;s=Temperature)&allowedSecurityPolicies=Basic256Sha256&keyStoreUrl=file:/etc/opcua/client.p12&keyStorePassword=RAW(changeit)&keyAlias=client&keyPassword=RAW(changeit)&trustListDirectory=/etc/opcua/pki +---- + ==== Milo 1.0.5 Migration Notes With Milo 1.0.5, several security-related API changes were introduced: * The `ServerCertificateValidator` class has been replaced with `CertificateValidator` in the core security package * Certificate validation now uses `org.eclipse.milo.opcua.stack.core.security.CertificateValidator` -* When configuring custom certificate validators, ensure you're using the updated API +* A validator configured with the `certificateValidator` option must implement this interface diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseComponentConfigurer.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseComponentConfigurer.java index b7046b96293b1..af20ae1082166 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseComponentConfigurer.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseComponentConfigurer.java @@ -38,6 +38,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "applicationUri": getOrCreateConfiguration(target).setApplicationUri(property(camelContext, java.lang.String.class, value)); return true; case "autowiredenabled": case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true; + case "certificatevalidator": + case "certificateValidator": getOrCreateConfiguration(target).setCertificateValidator(property(camelContext, org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class, value)); return true; case "channellifetime": case "channelLifetime": getOrCreateConfiguration(target).setChannelLifetime(property(camelContext, java.lang.Long.class, value)); return true; case "clientid": @@ -80,6 +82,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "sessionName": getOrCreateConfiguration(target).setSessionName(property(camelContext, java.lang.String.class, value)); return true; case "sessiontimeout": case "sessionTimeout": getOrCreateConfiguration(target).setSessionTimeout(property(camelContext, java.lang.Long.class, value)); return true; + case "trustallcertificates": + case "trustAllCertificates": getOrCreateConfiguration(target).setTrustAllCertificates(property(camelContext, boolean.class, value)); return true; + case "trustlistdirectory": + case "trustListDirectory": getOrCreateConfiguration(target).setTrustListDirectory(property(camelContext, java.lang.String.class, value)); return true; case "username": getOrCreateConfiguration(target).setUsername(property(camelContext, java.lang.String.class, value)); return true; default: return false; } @@ -101,6 +107,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "applicationUri": return java.lang.String.class; case "autowiredenabled": case "autowiredEnabled": return boolean.class; + case "certificatevalidator": + case "certificateValidator": return org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class; case "channellifetime": case "channelLifetime": return java.lang.Long.class; case "clientid": @@ -143,6 +151,10 @@ public Class getOptionType(String name, boolean ignoreCase) { case "sessionName": return java.lang.String.class; case "sessiontimeout": case "sessionTimeout": return java.lang.Long.class; + case "trustallcertificates": + case "trustAllCertificates": return boolean.class; + case "trustlistdirectory": + case "trustListDirectory": return java.lang.String.class; case "username": return java.lang.String.class; default: return null; } @@ -160,6 +172,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "applicationUri": return getOrCreateConfiguration(target).getApplicationUri(); case "autowiredenabled": case "autowiredEnabled": return target.isAutowiredEnabled(); + case "certificatevalidator": + case "certificateValidator": return getOrCreateConfiguration(target).getCertificateValidator(); case "channellifetime": case "channelLifetime": return getOrCreateConfiguration(target).getChannelLifetime(); case "clientid": @@ -202,6 +216,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "sessionName": return getOrCreateConfiguration(target).getSessionName(); case "sessiontimeout": case "sessionTimeout": return getOrCreateConfiguration(target).getSessionTimeout(); + case "trustallcertificates": + case "trustAllCertificates": return getOrCreateConfiguration(target).isTrustAllCertificates(); + case "trustlistdirectory": + case "trustListDirectory": return getOrCreateConfiguration(target).getTrustListDirectory(); case "username": return getOrCreateConfiguration(target).getUsername(); default: return null; } diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointConfigurer.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointConfigurer.java index aa088f47c678e..b67504f356a51 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointConfigurer.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointConfigurer.java @@ -29,6 +29,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "applicationName": target.getConfiguration().setApplicationName(property(camelContext, java.lang.String.class, value)); return true; case "applicationuri": case "applicationUri": target.getConfiguration().setApplicationUri(property(camelContext, java.lang.String.class, value)); return true; + case "certificatevalidator": + case "certificateValidator": target.getConfiguration().setCertificateValidator(property(camelContext, org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class, value)); return true; case "channellifetime": case "channelLifetime": target.getConfiguration().setChannelLifetime(property(camelContext, java.lang.Long.class, value)); return true; case "clientid": @@ -79,6 +81,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "sessionName": target.getConfiguration().setSessionName(property(camelContext, java.lang.String.class, value)); return true; case "sessiontimeout": case "sessionTimeout": target.getConfiguration().setSessionTimeout(property(camelContext, java.lang.Long.class, value)); return true; + case "trustallcertificates": + case "trustAllCertificates": target.getConfiguration().setTrustAllCertificates(property(camelContext, boolean.class, value)); return true; + case "trustlistdirectory": + case "trustListDirectory": target.getConfiguration().setTrustListDirectory(property(camelContext, java.lang.String.class, value)); return true; case "username": target.getConfiguration().setUsername(property(camelContext, java.lang.String.class, value)); return true; default: return false; } @@ -93,6 +99,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "applicationName": return java.lang.String.class; case "applicationuri": case "applicationUri": return java.lang.String.class; + case "certificatevalidator": + case "certificateValidator": return org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class; case "channellifetime": case "channelLifetime": return java.lang.Long.class; case "clientid": @@ -143,6 +151,10 @@ public Class getOptionType(String name, boolean ignoreCase) { case "sessionName": return java.lang.String.class; case "sessiontimeout": case "sessionTimeout": return java.lang.Long.class; + case "trustallcertificates": + case "trustAllCertificates": return boolean.class; + case "trustlistdirectory": + case "trustListDirectory": return java.lang.String.class; case "username": return java.lang.String.class; default: return null; } @@ -158,6 +170,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "applicationName": return target.getConfiguration().getApplicationName(); case "applicationuri": case "applicationUri": return target.getConfiguration().getApplicationUri(); + case "certificatevalidator": + case "certificateValidator": return target.getConfiguration().getCertificateValidator(); case "channellifetime": case "channelLifetime": return target.getConfiguration().getChannelLifetime(); case "clientid": @@ -208,6 +222,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "sessionName": return target.getConfiguration().getSessionName(); case "sessiontimeout": case "sessionTimeout": return target.getConfiguration().getSessionTimeout(); + case "trustallcertificates": + case "trustAllCertificates": return target.getConfiguration().isTrustAllCertificates(); + case "trustlistdirectory": + case "trustListDirectory": return target.getConfiguration().getTrustListDirectory(); case "username": return target.getConfiguration().getUsername(); default: return null; } diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointUriFactory.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointUriFactory.java index 5c7240ca16829..4bfbc6a6a1ffd 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointUriFactory.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/browse/MiloBrowseEndpointUriFactory.java @@ -24,10 +24,11 @@ public class MiloBrowseEndpointUriFactory extends org.apache.camel.support.compo private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(33); + Set props = new HashSet<>(36); props.add("allowedSecurityPolicies"); props.add("applicationName"); props.add("applicationUri"); + props.add("certificateValidator"); props.add("channelLifetime"); props.add("clientId"); props.add("depth"); @@ -57,6 +58,8 @@ public class MiloBrowseEndpointUriFactory extends org.apache.camel.support.compo props.add("requestedPublishingInterval"); props.add("sessionName"); props.add("sessionTimeout"); + props.add("trustAllCertificates"); + props.add("trustListDirectory"); props.add("username"); PROPERTY_NAMES = Collections.unmodifiableSet(props); Set secretProps = new HashSet<>(3); diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientComponentConfigurer.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientComponentConfigurer.java index 055e23d8baf8c..a360bc71a2c0d 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientComponentConfigurer.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientComponentConfigurer.java @@ -40,6 +40,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true; case "bridgeerrorhandler": case "bridgeErrorHandler": target.setBridgeErrorHandler(property(camelContext, boolean.class, value)); return true; + case "certificatevalidator": + case "certificateValidator": getOrCreateConfiguration(target).setCertificateValidator(property(camelContext, org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class, value)); return true; case "channellifetime": case "channelLifetime": getOrCreateConfiguration(target).setChannelLifetime(property(camelContext, java.lang.Long.class, value)); return true; case "clientid": @@ -82,6 +84,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "sessionName": getOrCreateConfiguration(target).setSessionName(property(camelContext, java.lang.String.class, value)); return true; case "sessiontimeout": case "sessionTimeout": getOrCreateConfiguration(target).setSessionTimeout(property(camelContext, java.lang.Long.class, value)); return true; + case "trustallcertificates": + case "trustAllCertificates": getOrCreateConfiguration(target).setTrustAllCertificates(property(camelContext, boolean.class, value)); return true; + case "trustlistdirectory": + case "trustListDirectory": getOrCreateConfiguration(target).setTrustListDirectory(property(camelContext, java.lang.String.class, value)); return true; case "username": getOrCreateConfiguration(target).setUsername(property(camelContext, java.lang.String.class, value)); return true; default: return false; } @@ -105,6 +111,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "autowiredEnabled": return boolean.class; case "bridgeerrorhandler": case "bridgeErrorHandler": return boolean.class; + case "certificatevalidator": + case "certificateValidator": return org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class; case "channellifetime": case "channelLifetime": return java.lang.Long.class; case "clientid": @@ -147,6 +155,10 @@ public Class getOptionType(String name, boolean ignoreCase) { case "sessionName": return java.lang.String.class; case "sessiontimeout": case "sessionTimeout": return java.lang.Long.class; + case "trustallcertificates": + case "trustAllCertificates": return boolean.class; + case "trustlistdirectory": + case "trustListDirectory": return java.lang.String.class; case "username": return java.lang.String.class; default: return null; } @@ -166,6 +178,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "autowiredEnabled": return target.isAutowiredEnabled(); case "bridgeerrorhandler": case "bridgeErrorHandler": return target.isBridgeErrorHandler(); + case "certificatevalidator": + case "certificateValidator": return getOrCreateConfiguration(target).getCertificateValidator(); case "channellifetime": case "channelLifetime": return getOrCreateConfiguration(target).getChannelLifetime(); case "clientid": @@ -208,6 +222,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "sessionName": return getOrCreateConfiguration(target).getSessionName(); case "sessiontimeout": case "sessionTimeout": return getOrCreateConfiguration(target).getSessionTimeout(); + case "trustallcertificates": + case "trustAllCertificates": return getOrCreateConfiguration(target).isTrustAllCertificates(); + case "trustlistdirectory": + case "trustListDirectory": return getOrCreateConfiguration(target).getTrustListDirectory(); case "username": return getOrCreateConfiguration(target).getUsername(); default: return null; } diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java index 971853e1a8e6a..3d59bd1c0aee3 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java @@ -31,6 +31,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "applicationUri": target.getConfiguration().setApplicationUri(property(camelContext, java.lang.String.class, value)); return true; case "bridgeerrorhandler": case "bridgeErrorHandler": target.setBridgeErrorHandler(property(camelContext, boolean.class, value)); return true; + case "certificatevalidator": + case "certificateValidator": target.getConfiguration().setCertificateValidator(property(camelContext, org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class, value)); return true; case "channellifetime": case "channelLifetime": target.getConfiguration().setChannelLifetime(property(camelContext, java.lang.Long.class, value)); return true; case "clientid": @@ -90,6 +92,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "sessionName": target.getConfiguration().setSessionName(property(camelContext, java.lang.String.class, value)); return true; case "sessiontimeout": case "sessionTimeout": target.getConfiguration().setSessionTimeout(property(camelContext, java.lang.Long.class, value)); return true; + case "trustallcertificates": + case "trustAllCertificates": target.getConfiguration().setTrustAllCertificates(property(camelContext, boolean.class, value)); return true; + case "trustlistdirectory": + case "trustListDirectory": target.getConfiguration().setTrustListDirectory(property(camelContext, java.lang.String.class, value)); return true; case "username": target.getConfiguration().setUsername(property(camelContext, java.lang.String.class, value)); return true; default: return false; } @@ -106,6 +112,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "applicationUri": return java.lang.String.class; case "bridgeerrorhandler": case "bridgeErrorHandler": return boolean.class; + case "certificatevalidator": + case "certificateValidator": return org.eclipse.milo.opcua.stack.core.security.CertificateValidator.class; case "channellifetime": case "channelLifetime": return java.lang.Long.class; case "clientid": @@ -165,6 +173,10 @@ public Class getOptionType(String name, boolean ignoreCase) { case "sessionName": return java.lang.String.class; case "sessiontimeout": case "sessionTimeout": return java.lang.Long.class; + case "trustallcertificates": + case "trustAllCertificates": return boolean.class; + case "trustlistdirectory": + case "trustListDirectory": return java.lang.String.class; case "username": return java.lang.String.class; default: return null; } @@ -182,6 +194,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "applicationUri": return target.getConfiguration().getApplicationUri(); case "bridgeerrorhandler": case "bridgeErrorHandler": return target.isBridgeErrorHandler(); + case "certificatevalidator": + case "certificateValidator": return target.getConfiguration().getCertificateValidator(); case "channellifetime": case "channelLifetime": return target.getConfiguration().getChannelLifetime(); case "clientid": @@ -241,6 +255,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "sessionName": return target.getConfiguration().getSessionName(); case "sessiontimeout": case "sessionTimeout": return target.getConfiguration().getSessionTimeout(); + case "trustallcertificates": + case "trustAllCertificates": return target.getConfiguration().isTrustAllCertificates(); + case "trustlistdirectory": + case "trustListDirectory": return target.getConfiguration().getTrustListDirectory(); case "username": return target.getConfiguration().getUsername(); default: return null; } diff --git a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json index 9bca6522e6575..b22c573e0c05b 100644 --- a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json +++ b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json @@ -49,8 +49,11 @@ "requestTimeout": { "index": 22, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 23, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 24, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 25, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 26, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The node ids.", "constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" } @@ -87,7 +90,10 @@ "requestTimeout": { "index": 28, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 29, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 30, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 31, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 32, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/components/camel-milo/src/main/docs/milo-browse-component.adoc b/components/camel-milo/src/main/docs/milo-browse-component.adoc index 3356e17c1e68a..a6d6a530733d5 100644 --- a/components/camel-milo/src/main/docs/milo-browse-component.adoc +++ b/components/camel-milo/src/main/docs/milo-browse-component.adoc @@ -54,7 +54,7 @@ include::partial$component-endpoint-headers.adoc[] === Client The browse component shares the same base options like the Camel Milo Client component, e.g. concerning topics like discovery, -security policies, the construction of node ids, etc. +security policies, the validation of the server certificate, the construction of node ids, etc. Please refer to the documentation of the Camel Milo Client component for further details. diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc b/components/camel-milo/src/main/docs/milo-client-component.adoc index 90ff4f0cb343f..9527e781cc490 100644 --- a/components/camel-milo/src/main/docs/milo-client-component.adoc +++ b/components/camel-milo/src/main/docs/milo-client-component.adoc @@ -203,10 +203,45 @@ The known security policy URIs and enum literals are can be seen here: https://g NOTE: In any case security policies are considered case sensitive. +When `allowedSecurityPolicies` is not set, the client accepts every security policy the server offers and uses the +endpoint with the highest security level reported by the server, which can be `None`. To make sure the connection is +secured, set `allowedSecurityPolicies` to the secured policies you expect the server to offer. + +=== Validating the server certificate + +When the selected endpoint uses a security policy other than `None`, the client validates the certificate of the +server when it opens the secure channel and the session. Configure the trusted certificates with one of these options: + +* `trustListDirectory`: a directory holding the trust list, in the layout used by Eclipse Milo. Put the certificate of +the server, or the certificate of the CA that issued it, in `trusted/certs`. Certificates of intermediate CAs that are +only used to build the chain go in `issuer/certs`, and certificate revocation lists in `trusted/crl` and `issuer/crl`. +The directories are created if they do not exist. A server certificate that fails validation is written to +`rejected/certs`, from where it can be moved to `trusted/certs` once it has been checked. +* `certificateValidator`: a custom `org.eclipse.milo.opcua.stack.core.security.CertificateValidator`. It is used +instead of `trustListDirectory` when both are set. + +The trust list checks that the certificate chains up to a trusted certificate. The optional checks of Eclipse Milo, +such as the validity period, the host name or the application URI, are only logged. To enforce them, configure a +`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the checks you need. + +If neither option is configured, the client does not connect to an endpoint that uses a security policy other than +`None`. Setting `trustAllCertificates` to `true` makes the client accept any server certificate instead. This is +insecure: the connection is encrypted, but the client does not authenticate the server it talks to. + +The same validator checks the server certificate that the user name and password are encrypted with. With the +security policy `None`, the server is not authenticated, whatever the configuration. + +A secured policy also requires a client certificate and key, configured with `keyStoreUrl` and the related options. +For example: + +---- +milo-client:opc.tcp://localhost:4840?node=RAW(ns=2;s=Temperature)&allowedSecurityPolicies=Basic256Sha256&keyStoreUrl=file:/etc/opcua/client.p12&keyStorePassword=RAW(changeit)&keyAlias=client&keyPassword=RAW(changeit)&trustListDirectory=/etc/opcua/pki +---- + ==== Milo 1.0.5 Migration Notes With Milo 1.0.5, several security-related API changes were introduced: * The `ServerCertificateValidator` class has been replaced with `CertificateValidator` in the core security package * Certificate validation now uses `org.eclipse.milo.opcua.stack.core.security.CertificateValidator` -* When configuring custom certificate validators, ensure you're using the updated API +* A validator configured with the `certificateValidator` option must implement this interface diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java index 6859b9fd896ad..17a30ad82b2ca 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java @@ -30,6 +30,7 @@ import org.apache.camel.spi.UriParam; import org.apache.camel.spi.UriParams; import org.eclipse.milo.opcua.sdk.client.OpcUaClientConfigBuilder; +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; 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.builtin.unsigned.UInteger; @@ -119,6 +120,15 @@ public class MiloClientConfiguration implements Cloneable { @UriParam(label = "security", security = "secret") private String password; + @UriParam(label = "security") + private String trustListDirectory; + + @UriParam(label = "security") + private CertificateValidator certificateValidator; + + @UriParam(label = "security", security = "insecure:ssl") + private boolean trustAllCertificates; + public MiloClientConfiguration() { } @@ -146,6 +156,9 @@ public MiloClientConfiguration(final MiloClientConfiguration other) { this.requestedPublishingInterval = other.requestedPublishingInterval; this.username = other.username; this.password = other.password; + this.trustListDirectory = other.trustListDirectory; + this.certificateValidator = other.certificateValidator; + this.trustAllCertificates = other.trustAllCertificates; } /** @@ -447,6 +460,46 @@ public String getPassword() { return this.password; } + /** + * The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by + * Eclipse Milo: trusted certificates in trusted/certs, CRLs of trusted certificates in trusted/crl, certificates of + * issuing CAs in issuer/certs and their CRLs in issuer/crl. The directories are created if they do not exist. A + * server certificate that fails validation is written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. + */ + public void setTrustListDirectory(final String trustListDirectory) { + this.trustListDirectory = trustListDirectory; + } + + public String getTrustListDirectory() { + return this.trustListDirectory; + } + + /** + * A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list + * configured with trustListDirectory. + */ + public void setCertificateValidator(final CertificateValidator certificateValidator) { + this.certificateValidator = certificateValidator; + } + + public CertificateValidator getCertificateValidator() { + return this.certificateValidator; + } + + /** + * Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, + * but the client does not authenticate the server it talks to. It only applies when neither certificateValidator + * nor trustListDirectory is configured. + */ + public void setTrustAllCertificates(final boolean trustAllCertificates) { + this.trustAllCertificates = trustAllCertificates; + } + + public boolean isTrustAllCertificates() { + return this.trustAllCertificates; + } + public Double getRequestedPublishingInterval() { return requestedPublishingInterval; } @@ -464,11 +517,27 @@ public MiloClientConfiguration clone() { } public String toCacheId() { + final String cacheId; if (this.clientId != null && !this.clientId.isEmpty()) { - return this.endpointUri + "|" + this.clientId; + cacheId = this.endpointUri + "|" + this.clientId; } else { - return this.endpointUri; + cacheId = this.endpointUri; + } + // endpoints that validate the server certificate differently must not share a connection + return cacheId + serverCertificateTrustCacheId(); + } + + private String serverCertificateTrustCacheId() { + if (this.certificateValidator != null) { + return "|certificateValidator=" + Integer.toHexString(System.identityHashCode(this.certificateValidator)); + } + if (this.trustListDirectory != null && !this.trustListDirectory.isEmpty()) { + return "|trustListDirectory=" + this.trustListDirectory; + } + if (this.trustAllCertificates) { + return "|trustAllCertificates"; } + return ""; } public OpcUaClientConfigBuilder newBuilder() { diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java new file mode 100644 index 0000000000000..33cd020e2db98 --- /dev/null +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java @@ -0,0 +1,163 @@ +/* + * 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.camel.component.milo.client.internal; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.camel.component.milo.client.MiloClientConfiguration; +import org.eclipse.milo.opcua.stack.core.security.CertificateQuarantine; +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.DefaultClientCertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.FileBasedCertificateQuarantine; +import org.eclipse.milo.opcua.stack.core.security.FileBasedTrustListManager; +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; + +/** + * Selects the validator for the certificate of the OPC UA server, and owns the file based trust list when one is + * configured. The trust list is created on first use and kept until {@link #close()}, so a reconnect reuses it. + */ +final class ServerCertificateTrust implements Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(ServerCertificateTrust.class); + + private final MiloClientConfiguration configuration; + private final Lock lock = new ReentrantLock(); + + private FileBasedTrustListManager trustListManager; + private CertificateValidator trustListValidator; + private boolean trustAllLogged; + private boolean closed; + + ServerCertificateTrust(final MiloClientConfiguration configuration) { + this.configuration = configuration; + } + + /** + * Returns the validator for the certificate of the server that provides the given endpoint. + * + * @param endpoint the endpoint selected from the discovery result, may be {@code null} + * @return the validator to use for the secure channel, the session and the identity token + * @throws IllegalStateException if the endpoint uses a security policy other than None and no way to validate the + * server certificate is configured + * @throws IOException if the trust list directory cannot be read or created + */ + CertificateValidator validatorFor(final EndpointDescription endpoint) throws IOException { + if (this.configuration.getCertificateValidator() != null) { + return this.configuration.getCertificateValidator(); + } + + final String trustListDirectory = this.configuration.getTrustListDirectory(); + if (trustListDirectory != null && !trustListDirectory.isEmpty()) { + return trustListValidator(Paths.get(trustListDirectory)); + } + + if (this.configuration.isTrustAllCertificates()) { + if (isSecured(endpoint)) { + logTrustAllOnce(); + } + return new CertificateValidator.InsecureCertificateValidator(); + } + + if (isSecured(endpoint)) { + throw new IllegalStateException( + "The certificate of the OPC UA server cannot be validated: endpoint " + endpoint.getEndpointUrl() + + " uses security policy " + endpoint.getSecurityPolicyUri() + + ", but neither trustListDirectory nor certificateValidator is configured." + + " Configure one of them, or set trustAllCertificates=true to accept any" + + " server certificate (insecure)."); + } + + // SecurityPolicy None: the server is not authenticated in any case, and its certificate is at most used to + // encrypt the user identity token + return new CertificateValidator.InsecureCertificateValidator(); + } + + /** + * Whether the endpoint uses a security policy other than None, in which case the server certificate is validated. + */ + static boolean isSecured(final EndpointDescription endpoint) { + return endpoint != null && !SecurityPolicy.None.getUri().equals(endpoint.getSecurityPolicyUri()); + } + + private CertificateValidator trustListValidator(final Path directory) throws IOException { + this.lock.lock(); + try { + if (this.closed) { + throw new IllegalStateException("The trust list in " + directory + " has been closed"); + } + if (this.trustListValidator == null) { + final CertificateQuarantine quarantine + = FileBasedCertificateQuarantine.create(directory.resolve("rejected").resolve("certs")); + final FileBasedTrustListManager manager = FileBasedTrustListManager.createAndInitialize(directory); + + if (manager.getTrustedCertificates().isEmpty() && manager.getIssuerCertificates().isEmpty()) { + LOG.warn("The trust list in {} contains no certificates, so the certificate of the OPC UA server will be" + + " rejected. Add the server certificate, or the certificate of its CA, to {}.", + directory, directory.resolve("trusted").resolve("certs")); + } + + this.trustListManager = manager; + this.trustListValidator = new DefaultClientCertificateValidator(manager, quarantine); + } + return this.trustListValidator; + } finally { + this.lock.unlock(); + } + } + + private void logTrustAllOnce() { + this.lock.lock(); + try { + if (this.trustAllLogged) { + return; + } + this.trustAllLogged = true; + } finally { + this.lock.unlock(); + } + LOG.warn("The milo client is configured with trustAllCertificates=true. The certificate of the OPC UA server is" + + " not validated, so the client does not authenticate the server it connects to. This should not be" + + " used in production environments."); + } + + @Override + public void close() throws IOException { + final FileBasedTrustListManager manager; + + this.lock.lock(); + try { + this.closed = true; + manager = this.trustListManager; + this.trustListManager = null; + this.trustListValidator = null; + } finally { + this.lock.unlock(); + } + + if (manager != null) { + manager.close(); + } + } +} diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java index ed98afb4cbc50..a71c169aa212a 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.milo.client.internal; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -57,6 +58,7 @@ import org.eclipse.milo.opcua.stack.core.NodeIds; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; 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.ExpandedNodeId; @@ -584,6 +586,7 @@ nodeId, direction, NodeIds.References, includeSubTypes, uint(nodeClasses), private final MiloClientConfiguration configuration; private final ScheduledExecutorService executor; private final long reconnectTimeout; + private final ServerCertificateTrust serverCertificateTrust; private final Lock lock = new ReentrantLock(); private Connected connected; @@ -597,6 +600,7 @@ public SubscriptionManager(final MiloClientConfiguration configuration, final Sc this.configuration = configuration; this.executor = executor; this.reconnectTimeout = reconnectTimeout; + this.serverCertificateTrust = new ServerCertificateTrust(configuration); connect(); } @@ -724,6 +728,9 @@ private Connected performConnect() throws Exception { LOG.debug("Selected endpoint: {}", endpoint); + // the same validator checks the server certificate for the secure channel, the session and the identity token + final CertificateValidator certificateValidator = this.serverCertificateTrust.validatorFor(endpoint); + // set identity providers final List providers = new LinkedList<>(); @@ -733,7 +740,8 @@ private Connected performConnect() throws Exception { final String explicitPassword = this.configuration.getPassword(); if (explicitUsername != null && !explicitUsername.isEmpty()) { LOG.debug("Enable username/password provider (explicit parameter): {}", explicitUsername); - providers.add(new UsernameProvider(explicitUsername, explicitPassword != null ? explicitPassword : "")); + providers.add(new UsernameProvider( + explicitUsername, explicitPassword != null ? explicitPassword : "", certificateValidator)); } else if (user != null && !user.isEmpty()) { final String[] creds = user.split(":", 2); if (creds != null) { @@ -741,7 +749,7 @@ private Connected performConnect() throws Exception { LOG.debug("Enable username/password provider: {}", creds[0]); } - providers.add(new UsernameProvider(creds[0], creds.length == 2 ? creds[1] : "")); + providers.add(new UsernameProvider(creds[0], creds.length == 2 ? creds[1] : "", certificateValidator)); } } @@ -750,6 +758,7 @@ private Connected performConnect() throws Exception { final OpcUaClientConfigBuilder cfg = this.configuration.newBuilder(); cfg.setIdentityProvider(new CompositeProvider(providers)); cfg.setEndpoint(endpoint); + cfg.setCertificateValidator(certificateValidator); // create client final OpcUaClient client = OpcUaClient.create( @@ -814,6 +823,12 @@ public void dispose() { // dispose outside of lock connected.dispose(); } + + try { + this.serverCertificateTrust.close(); + } catch (final IOException e) { + LOG.debug("Failed to close the trust list. This exception is ignored.", e); + } } private void triggerReconnect(final boolean immediate) { diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java new file mode 100644 index 0000000000000..30337115bef48 --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java @@ -0,0 +1,189 @@ +/* + * 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.camel.component.milo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.apache.camel.CamelContext; +import org.apache.camel.EndpointInject; +import org.apache.camel.Produce; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.milo.call.TestCertificateFactory; +import org.apache.camel.component.milo.server.MiloServerComponent; +import org.apache.camel.component.mock.MockEndpoint; +import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.DefaultApplicationGroup; +import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager; +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.MemoryCertificateStore; +import org.eclipse.milo.opcua.stack.core.security.MemoryTrustListManager; +import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The client validates the certificate of a server that only offers a secured endpoint. + */ +class ServerCertificateValidationTest extends AbstractMiloServerTest { + + private static final String DIRECT_START = "direct:start"; + + private static final String MILO_SERVER_ITEM = "milo-server:myitem1"; + + private static final String MILO_CLIENT = "milo-client:opc.tcp://foo:bar@localhost:@@port@@?node=" + + NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "myitem1") + + "&keyStoreUrl=file:src/test/resources/keystore&keyStorePassword=testtest" + + "&keyPassword=test&keyAlias=test&allowedSecurityPolicies=Basic256Sha256" + + "&discoveryEndpointSuffix=/discovery&overrideHost=true"; + + private static final String SERVER_APPLICATION_URI = "urn:eclipse:milo:test"; + + @TempDir + static Path pki; + + private final RecordingCertificateValidator recordingValidator = new RecordingCertificateValidator(); + + @EndpointInject("mock:trustList") + MockEndpoint trustListEndpoint; + + @EndpointInject("mock:emptyTrustList") + MockEndpoint emptyTrustListEndpoint; + + @EndpointInject("mock:notConfigured") + MockEndpoint notConfiguredEndpoint; + + @EndpointInject("mock:trustAll") + MockEndpoint trustAllEndpoint; + + @EndpointInject("mock:certificateValidator") + MockEndpoint certificateValidatorEndpoint; + + @Produce(DIRECT_START) + ProducerTemplate producer; + + @Override + protected void configureContext(final CamelContext context) throws Exception { + super.configureContext(context); + context.getRegistry().bind("recordingValidator", this.recordingValidator); + } + + @Override + protected void configureMiloServer(final MiloServerComponent server) throws Exception { + super.configureMiloServer(server); + + // the server uses a self-signed certificate, and accepts any client: the client is under test here + final DefaultApplicationGroup certificateGroup = DefaultApplicationGroup.createAndInitialize( + new MemoryTrustListManager(), new MemoryCertificateStore(), new TestCertificateFactory(), + new CertificateValidator.InsecureCertificateValidator()); + final X509Certificate serverCertificate = certificateGroup.getCertificateEntries().get(0).certificateChain[0]; + + server.setApplicationUri(SERVER_APPLICATION_URI); + server.setCertificateManager(new DefaultCertificateManager(new MemoryCertificateQuarantine(), certificateGroup)); + server.setCertificate(serverCertificate); + server.setSecurityPolicies(EnumSet.of(SecurityPolicy.Basic256Sha256)); + server.setUsernameSecurityPolicyUri(SecurityPolicy.Basic256Sha256); + + final Path trustedCerts = Files.createDirectories(pki.resolve("trusted").resolve("trusted").resolve("certs")); + Files.write(trustedCerts.resolve("server.der"), serverCertificate.getEncoded()); + + this.recordingValidator.trust(serverCertificate); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from(DIRECT_START).to(MILO_SERVER_ITEM); + + from(resolve(MILO_CLIENT + "&trustListDirectory=RAW(" + pki.resolve("trusted") + ")")) + .to("mock:trustList"); + from(resolve(MILO_CLIENT + "&trustListDirectory=RAW(" + pki.resolve("empty") + ")")) + .to("mock:emptyTrustList"); + from(resolve(MILO_CLIENT)).to("mock:notConfigured"); + from(resolve(MILO_CLIENT + "&trustAllCertificates=true")).to("mock:trustAll"); + from(resolve(MILO_CLIENT + "&certificateValidator=#recordingValidator")).to("mock:certificateValidator"); + } + }; + } + + @Test + void onlyClientsThatCanValidateTheServerCertificateConnect() throws Exception { + this.trustListEndpoint.setExpectedCount(1); + this.trustAllEndpoint.setExpectedCount(1); + this.certificateValidatorEndpoint.setExpectedCount(1); + + this.emptyTrustListEndpoint.setExpectedCount(0); + this.emptyTrustListEndpoint.setSleepForEmptyTest(5_000); + this.notConfiguredEndpoint.setExpectedCount(0); + this.notConfiguredEndpoint.setSleepForEmptyTest(5_000); + + this.producer.sendBody("Foo"); + + MockEndpoint.assertIsSatisfied(context); + + // the server certificate rejected by the empty trust list is kept for review + final Path rejectedCerts = pki.resolve("empty").resolve("rejected").resolve("certs"); + await().atMost(10, TimeUnit.SECONDS).until(() -> countFiles(rejectedCerts) > 0); + + assertTrue(this.recordingValidator.validations.get() > 0, "The configured certificate validator was not used"); + } + + private static long countFiles(final Path directory) throws IOException { + if (!Files.isDirectory(directory)) { + return 0; + } + try (Stream files = Files.list(directory)) { + return files.count(); + } + } + + private static final class RecordingCertificateValidator implements CertificateValidator { + + private final AtomicInteger validations = new AtomicInteger(); + private final MemoryTrustListManager trustList = new MemoryTrustListManager(); + private final CertificateValidator delegate + = new DefaultClientCertificateValidator(this.trustList, new MemoryCertificateQuarantine()); + + void trust(final X509Certificate certificate) { + this.trustList.addTrustedCertificate(certificate); + } + + @Override + public void validateCertificateChain( + final List certificateChain, final String applicationUri, final String[] validHostNames) + throws UaException { + this.validations.incrementAndGet(); + this.delegate.validateCertificateChain(certificateChain, applicationUri, validHostNames); + } + } +} diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java new file mode 100644 index 0000000000000..68d0a6a7c4157 --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java @@ -0,0 +1,83 @@ +/* + * 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.camel.component.milo.client; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MiloClientConfigurationTest { + + private static final String ENDPOINT_URI = "opc.tcp://localhost:4840"; + + @Test + void cacheIdIsUnchangedWithoutServerCertificateTrustSettings() { + assertEquals(ENDPOINT_URI + "|1000.0", configuration().toCacheId()); + } + + @Test + void endpointsValidatingTheServerDifferentlyDoNotShareAConnection() { + final MiloClientConfiguration trustAll = configuration(); + trustAll.setTrustAllCertificates(true); + final MiloClientConfiguration trustList = configuration(); + trustList.setTrustListDirectory("pki"); + final MiloClientConfiguration otherTrustList = configuration(); + otherTrustList.setTrustListDirectory("other-pki"); + final MiloClientConfiguration validator = configuration(); + validator.setCertificateValidator(validator()); + + final Set cacheIds = new HashSet<>( + Arrays.asList( + configuration().toCacheId(), trustAll.toCacheId(), trustList.toCacheId(), otherTrustList.toCacheId(), + validator.toCacheId())); + + assertEquals(5, cacheIds.size()); + } + + @Test + void copyAndCloneKeepServerCertificateTrustSettings() { + final CertificateValidator certificateValidator = validator(); + final MiloClientConfiguration original = configuration(); + original.setTrustListDirectory("pki"); + original.setCertificateValidator(certificateValidator); + original.setTrustAllCertificates(true); + + for (final MiloClientConfiguration copy : Arrays.asList(new MiloClientConfiguration(original), original.clone())) { + assertEquals("pki", copy.getTrustListDirectory()); + assertSame(certificateValidator, copy.getCertificateValidator()); + assertTrue(copy.isTrustAllCertificates()); + } + } + + private static MiloClientConfiguration configuration() { + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setEndpointUri(ENDPOINT_URI); + return configuration; + } + + private static CertificateValidator validator() { + return (certificateChain, applicationUri, validHostNames) -> { + }; + } +} diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java new file mode 100644 index 0000000000000..d9f33c6bc502e --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java @@ -0,0 +1,162 @@ +/* + * 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.camel.component.milo.client.internal; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.camel.component.milo.call.TestCertificateFactory; +import org.apache.camel.component.milo.client.MiloClientConfiguration; +import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; +import org.eclipse.milo.opcua.stack.core.security.DefaultClientCertificateValidator; +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.builtin.unsigned.UByte; +import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; +import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServerCertificateTrustTest { + + private static X509Certificate serverCertificate; + + @TempDir + Path trustListDirectory; + + @BeforeAll + static void createServerCertificate() { + final TestCertificateFactory certificateFactory = new TestCertificateFactory(); + final KeyPair keyPair = certificateFactory.createRsaSha256KeyPair(); + serverCertificate = certificateFactory.createRsaSha256CertificateChain(keyPair)[0]; + } + + @Test + void securedEndpointWithoutTrustConfigurationIsRefused() { + final ServerCertificateTrust trust = new ServerCertificateTrust(new MiloClientConfiguration()); + + final IllegalStateException e + = assertThrows(IllegalStateException.class, () -> trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + + assertTrue(e.getMessage().contains("trustListDirectory"), e.getMessage()); + assertTrue(e.getMessage().contains("certificateValidator"), e.getMessage()); + assertTrue(e.getMessage().contains("trustAllCertificates"), e.getMessage()); + } + + @Test + void endpointWithoutSecurityDoesNotNeedTrustConfiguration() throws Exception { + final ServerCertificateTrust trust = new ServerCertificateTrust(new MiloClientConfiguration()); + + assertInstanceOf(CertificateValidator.InsecureCertificateValidator.class, + trust.validatorFor(endpoint(SecurityPolicy.None))); + } + + @Test + void trustAllCertificatesAcceptsAnyServerCertificate() throws Exception { + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setTrustAllCertificates(true); + final ServerCertificateTrust trust = new ServerCertificateTrust(configuration); + + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + assertDoesNotThrow(() -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + } + + @Test + void certificateValidatorIsUsedBeforeOtherOptions() throws Exception { + final CertificateValidator custom = (certificateChain, applicationUri, validHostNames) -> { + }; + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setCertificateValidator(custom); + configuration.setTrustListDirectory(this.trustListDirectory.toString()); + configuration.setTrustAllCertificates(true); + + try (ServerCertificateTrust trust = new ServerCertificateTrust(configuration)) { + assertSame(custom, trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + } + } + + @Test + void trustListAcceptsTrustedServerCertificate() throws Exception { + final Path trustedCerts = Files.createDirectories(this.trustListDirectory.resolve("trusted").resolve("certs")); + Files.write(trustedCerts.resolve("server.der"), serverCertificate.getEncoded()); + + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setTrustListDirectory(this.trustListDirectory.toString()); + + try (ServerCertificateTrust trust = new ServerCertificateTrust(configuration)) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + assertInstanceOf(DefaultClientCertificateValidator.class, validator); + assertDoesNotThrow(() -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + // a reconnect uses the same trust list + assertSame(validator, trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + } + } + + @Test + void trustListRejectsAndKeepsUntrustedServerCertificate() throws Exception { + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setTrustListDirectory(this.trustListDirectory.toString()); + + try (ServerCertificateTrust trust = new ServerCertificateTrust(configuration)) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + assertThrows(UaException.class, + () -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + } + + assertTrue(Files.isDirectory(this.trustListDirectory.resolve("trusted").resolve("certs"))); + assertTrue(Files.isDirectory(this.trustListDirectory.resolve("issuer").resolve("certs"))); + try (Stream rejected = Files.list(this.trustListDirectory.resolve("rejected").resolve("certs"))) { + assertEquals(1, rejected.count()); + } + } + + @Test + void closedTrustListIsNotReused() throws Exception { + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setTrustListDirectory(this.trustListDirectory.toString()); + final ServerCertificateTrust trust = new ServerCertificateTrust(configuration); + trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + trust.close(); + + assertThrows(IllegalStateException.class, () -> trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + } + + private static EndpointDescription endpoint(final SecurityPolicy securityPolicy) { + final MessageSecurityMode securityMode + = securityPolicy == SecurityPolicy.None ? MessageSecurityMode.None : MessageSecurityMode.SignAndEncrypt; + return new EndpointDescription( + "opc.tcp://localhost:4840", null, ByteString.NULL_VALUE, securityMode, securityPolicy.getUri(), null, null, + UByte.valueOf(0)); + } +} diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java index 7f9b98d8a879e..ee2fe9ba66c33 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java @@ -462,6 +462,24 @@ default MiloBrowseComponentBuilder sessionTimeout(java.lang.Long sessionTimeout) return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * <code>org.eclipse.milo.opcua.stack.core.security.CertificateValidator</code> type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloBrowseComponentBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -479,6 +497,48 @@ default MiloBrowseComponentBuilder password(java.lang.String password) { return this; } + + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloBrowseComponentBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloBrowseComponentBuilder trustListDirectory(java.lang.String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } + /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special @@ -541,7 +601,10 @@ protected boolean setPropertyOnComponent( case "requestTimeout": getOrCreateConfiguration((MiloBrowseComponent) component).setRequestTimeout((java.lang.Long) value); return true; case "sessionName": getOrCreateConfiguration((MiloBrowseComponent) component).setSessionName((java.lang.String) value); return true; case "sessionTimeout": getOrCreateConfiguration((MiloBrowseComponent) component).setSessionTimeout((java.lang.Long) value); return true; + case "certificateValidator": getOrCreateConfiguration((MiloBrowseComponent) component).setCertificateValidator((org.eclipse.milo.opcua.stack.core.security.CertificateValidator) value); return true; case "password": getOrCreateConfiguration((MiloBrowseComponent) component).setPassword((java.lang.String) value); return true; + case "trustAllCertificates": getOrCreateConfiguration((MiloBrowseComponent) component).setTrustAllCertificates((boolean) value); return true; + case "trustListDirectory": getOrCreateConfiguration((MiloBrowseComponent) component).setTrustListDirectory((java.lang.String) value); return true; case "username": getOrCreateConfiguration((MiloBrowseComponent) component).setUsername((java.lang.String) value); return true; default: return false; } diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java index 44d22e8023a0d..c2104d30a32fb 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java @@ -490,6 +490,24 @@ default MiloClientComponentBuilder sessionTimeout(java.lang.Long sessionTimeout) return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * <code>org.eclipse.milo.opcua.stack.core.security.CertificateValidator</code> type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientComponentBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -507,6 +525,48 @@ default MiloClientComponentBuilder password(java.lang.String password) { return this; } + + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientComponentBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloClientComponentBuilder trustListDirectory(java.lang.String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } + /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special @@ -570,7 +630,10 @@ protected boolean setPropertyOnComponent( case "requestTimeout": getOrCreateConfiguration((MiloClientComponent) component).setRequestTimeout((java.lang.Long) value); return true; case "sessionName": getOrCreateConfiguration((MiloClientComponent) component).setSessionName((java.lang.String) value); return true; case "sessionTimeout": getOrCreateConfiguration((MiloClientComponent) component).setSessionTimeout((java.lang.Long) value); return true; + case "certificateValidator": getOrCreateConfiguration((MiloClientComponent) component).setCertificateValidator((org.eclipse.milo.opcua.stack.core.security.CertificateValidator) value); return true; case "password": getOrCreateConfiguration((MiloClientComponent) component).setPassword((java.lang.String) value); return true; + case "trustAllCertificates": getOrCreateConfiguration((MiloClientComponent) component).setTrustAllCertificates((boolean) value); return true; + case "trustListDirectory": getOrCreateConfiguration((MiloClientComponent) component).setTrustListDirectory((java.lang.String) value); return true; case "username": getOrCreateConfiguration((MiloClientComponent) component).setUsername((java.lang.String) value); return true; default: return false; } diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java index 76ff5d3c39693..6d0c0156403bd 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java @@ -685,6 +685,40 @@ default MiloBrowseEndpointBuilder sessionTimeout(String sessionTimeout) { doSetProperty("sessionTimeout", sessionTimeout); return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option will be converted to a + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -701,6 +735,63 @@ default MiloBrowseEndpointBuilder password(String password) { doSetProperty("password", password); return this; } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder trustAllCertificates(String trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: java.lang.String type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder trustListDirectory(String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java index 8c00914b1ba4e..a1a5f2e05824c 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java @@ -709,6 +709,40 @@ default MiloClientEndpointConsumerBuilder sessionTimeout(String sessionTimeout) doSetProperty("sessionTimeout", sessionTimeout); return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option will be converted to a + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -725,6 +759,63 @@ default MiloClientEndpointConsumerBuilder password(String password) { doSetProperty("password", password); return this; } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder trustAllCertificates(String trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: java.lang.String type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder trustListDirectory(String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special @@ -1546,6 +1637,40 @@ default MiloClientEndpointProducerBuilder sessionTimeout(String sessionTimeout) doSetProperty("sessionTimeout", sessionTimeout); return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option will be converted to a + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -1562,6 +1687,63 @@ default MiloClientEndpointProducerBuilder password(String password) { doSetProperty("password", password); return this; } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder trustAllCertificates(String trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: java.lang.String type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder trustListDirectory(String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special @@ -2312,6 +2494,40 @@ default MiloClientEndpointBuilder sessionTimeout(String sessionTimeout) { doSetProperty("sessionTimeout", sessionTimeout); return this; } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option is a: + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder certificateValidator(org.eclipse.milo.opcua.stack.core.security.CertificateValidator certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } + /** + * A custom validator for the certificate of the OPC UA server. When + * set, it is used instead of the trust list configured with + * trustListDirectory. + * + * The option will be converted to a + * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + return this; + } /** * The password for authentication. Use this instead of embedding * credentials in the endpoint URI when the password contains special @@ -2328,6 +2544,63 @@ default MiloClientEndpointBuilder password(String password) { doSetProperty("password", password); return this; } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option is a: boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder trustAllCertificates(boolean trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * Whether to accept any certificate presented by the OPC UA server. + * This is insecure: the connection is encrypted, but the client does + * not authenticate the server it talks to. It only applies when neither + * certificateValidator nor trustListDirectory is configured. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: security + * + * @param trustAllCertificates the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder trustAllCertificates(String trustAllCertificates) { + doSetProperty("trustAllCertificates", trustAllCertificates); + return this; + } + /** + * The directory holding the trust list used to validate the certificate + * of the OPC UA server, in the layout used by Eclipse Milo: trusted + * certificates in trusted/certs, CRLs of trusted certificates in + * trusted/crl, certificates of issuing CAs in issuer/certs and their + * CRLs in issuer/crl. The directories are created if they do not exist. + * A server certificate that fails validation is written to + * rejected/certs, from where it can be moved to trusted/certs once it + * has been checked. + * + * The option is a: java.lang.String type. + * + * Group: security + * + * @param trustListDirectory the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder trustListDirectory(String trustListDirectory) { + doSetProperty("trustListDirectory", trustListDirectory); + return this; + } /** * The username for authentication. Use this instead of embedding * credentials in the endpoint URI when the username contains special From 71ccce2fe52a44febd53eafbceca65c33c4988fc Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Thu, 15 Oct 2026 10:47:33 +0200 Subject: [PATCH 2/5] CAMEL-25645: camel-milo - address review feedback The trust list validator checks the application URI, the validity period and the revocation status of the server certificate. As OPC UA requires, a certificate issued by a CA needs a revocation list of that CA in the trust list. The documentation describes these checks exactly. A missing trust configuration and a failed certificate or security check are logged at WARN, and the last connection failure is the cause of the "No connected" exception that a producer gets. The end-to-end test asserts that reason, and the unit tests cover revoked, expired, CA without revocation list and application URI cases. The connection cache id uses the class name of a custom validator. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Andrea Cosentino --- .../camel/catalog/components/milo-browse.json | 8 +- .../catalog/docs/milo-client-component.adoc | 26 ++- .../component/milo/browse/milo-browse.json | 8 +- .../src/main/docs/milo-client-component.adoc | 26 ++- .../milo/client/MiloClientConfiguration.java | 12 +- .../internal/ServerCertificateTrust.java | 86 +++++++-- .../client/internal/SubscriptionManager.java | 27 ++- .../milo/ServerCertificateValidationTest.java | 27 ++- .../internal/ServerCertificateTrustTest.java | 164 +++++++++++++++--- .../milo/client/internal/TestPki.java | 145 ++++++++++++++++ .../MiloBrowseComponentBuilderFactory.java | 11 +- .../MiloClientComponentBuilderFactory.java | 11 +- .../dsl/MiloBrowseEndpointBuilderFactory.java | 14 +- .../dsl/MiloClientEndpointBuilderFactory.java | 42 +++-- 14 files changed, 513 insertions(+), 94 deletions(-) create mode 100644 components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/TestPki.java diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json index b22c573e0c05b..21f0f5d866875 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json @@ -49,10 +49,10 @@ "requestTimeout": { "index": 22, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 23, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 24, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { @@ -90,10 +90,10 @@ "requestTimeout": { "index": 28, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 29, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 30, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc index 9527e781cc490..29627f9960d3b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc @@ -215,19 +215,33 @@ server when it opens the secure channel and the session. Configure the trusted c * `trustListDirectory`: a directory holding the trust list, in the layout used by Eclipse Milo. Put the certificate of the server, or the certificate of the CA that issued it, in `trusted/certs`. Certificates of intermediate CAs that are only used to build the chain go in `issuer/certs`, and certificate revocation lists in `trusted/crl` and `issuer/crl`. -The directories are created if they do not exist. A server certificate that fails validation is written to -`rejected/certs`, from where it can be moved to `trusted/certs` once it has been checked. +The directories are created if they do not exist. * `certificateValidator`: a custom `org.eclipse.milo.opcua.stack.core.security.CertificateValidator`. It is used -instead of `trustListDirectory` when both are set. +instead of `trustListDirectory` when both are set. Endpoints share a connection only when they use the same validator +instance. -The trust list checks that the certificate chains up to a trusted certificate. The optional checks of Eclipse Milo, -such as the validity period, the host name or the application URI, are only logged. To enforce them, configure a -`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the checks you need. +With `trustListDirectory`, the client refuses a server certificate that: + +* does not chain up to a certificate in `trusted/certs`, possibly through the certificates in `issuer/certs`, +* is outside its validity period, +* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. When the trust list has no revocation +list for an issuer, the revocation status of the certificates it issued is not checked, +* or holds an application URI that differs from the one the server announces in its endpoint description. + +A failed host name, key usage or extended key usage check is only logged. A certificate that does not chain up to a +trusted certificate is written to `rejected/certs`, from where it can be moved to `trusted/certs` once it has been +checked. A certificate refused for another reason is not written there. To use other checks, configure a +`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the `ValidationCheck` set you +need. If neither option is configured, the client does not connect to an endpoint that uses a security policy other than `None`. Setting `trustAllCertificates` to `true` makes the client accept any server certificate instead. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. +When the server certificate cannot be validated, the client logs a `WARN` message that starts with `Failed to +connect`, and tries again every 10 seconds, so a certificate moved to `trusted/certs` is used at the next attempt. A +producer fails with `IllegalStateException: No connected`, whose cause gives the reason. + The same validator checks the server certificate that the user name and password are encrypted with. With the security policy `None`, the server is not authenticated, whatever the configuration. diff --git a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json index b22c573e0c05b..21f0f5d866875 100644 --- a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json +++ b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json @@ -49,10 +49,10 @@ "requestTimeout": { "index": 22, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 23, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 24, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { @@ -90,10 +90,10 @@ "requestTimeout": { "index": 28, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 29, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 30, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory." }, + "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. A server certificate that fails validation is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc b/components/camel-milo/src/main/docs/milo-client-component.adoc index 9527e781cc490..29627f9960d3b 100644 --- a/components/camel-milo/src/main/docs/milo-client-component.adoc +++ b/components/camel-milo/src/main/docs/milo-client-component.adoc @@ -215,19 +215,33 @@ server when it opens the secure channel and the session. Configure the trusted c * `trustListDirectory`: a directory holding the trust list, in the layout used by Eclipse Milo. Put the certificate of the server, or the certificate of the CA that issued it, in `trusted/certs`. Certificates of intermediate CAs that are only used to build the chain go in `issuer/certs`, and certificate revocation lists in `trusted/crl` and `issuer/crl`. -The directories are created if they do not exist. A server certificate that fails validation is written to -`rejected/certs`, from where it can be moved to `trusted/certs` once it has been checked. +The directories are created if they do not exist. * `certificateValidator`: a custom `org.eclipse.milo.opcua.stack.core.security.CertificateValidator`. It is used -instead of `trustListDirectory` when both are set. +instead of `trustListDirectory` when both are set. Endpoints share a connection only when they use the same validator +instance. -The trust list checks that the certificate chains up to a trusted certificate. The optional checks of Eclipse Milo, -such as the validity period, the host name or the application URI, are only logged. To enforce them, configure a -`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the checks you need. +With `trustListDirectory`, the client refuses a server certificate that: + +* does not chain up to a certificate in `trusted/certs`, possibly through the certificates in `issuer/certs`, +* is outside its validity period, +* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. When the trust list has no revocation +list for an issuer, the revocation status of the certificates it issued is not checked, +* or holds an application URI that differs from the one the server announces in its endpoint description. + +A failed host name, key usage or extended key usage check is only logged. A certificate that does not chain up to a +trusted certificate is written to `rejected/certs`, from where it can be moved to `trusted/certs` once it has been +checked. A certificate refused for another reason is not written there. To use other checks, configure a +`certificateValidator`, for example a `DefaultClientCertificateValidator` created with the `ValidationCheck` set you +need. If neither option is configured, the client does not connect to an endpoint that uses a security policy other than `None`. Setting `trustAllCertificates` to `true` makes the client accept any server certificate instead. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. +When the server certificate cannot be validated, the client logs a `WARN` message that starts with `Failed to +connect`, and tries again every 10 seconds, so a certificate moved to `trusted/certs` is used at the next attempt. A +producer fails with `IllegalStateException: No connected`, whose cause gives the reason. + The same validator checks the server certificate that the user name and password are encrypted with. With the security policy `None`, the server is not authenticated, whatever the configuration. diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java index 17a30ad82b2ca..2354caa26b4c9 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java @@ -463,9 +463,10 @@ public String getPassword() { /** * The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by * Eclipse Milo: trusted certificates in trusted/certs, CRLs of trusted certificates in trusted/crl, certificates of - * issuing CAs in issuer/certs and their CRLs in issuer/crl. The directories are created if they do not exist. A - * server certificate that fails validation is written to rejected/certs, from where it can be moved to - * trusted/certs once it has been checked. + * issuing CAs in issuer/certs and their CRLs in issuer/crl. The directories are created if they do not exist. + * Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application + * URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate + * is written to rejected/certs, from where it can be moved to trusted/certs once it has been checked. */ public void setTrustListDirectory(final String trustListDirectory) { this.trustListDirectory = trustListDirectory; @@ -477,7 +478,7 @@ public String getTrustListDirectory() { /** * A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list - * configured with trustListDirectory. + * configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance. */ public void setCertificateValidator(final CertificateValidator certificateValidator) { this.certificateValidator = certificateValidator; @@ -529,7 +530,8 @@ public String toCacheId() { private String serverCertificateTrustCacheId() { if (this.certificateValidator != null) { - return "|certificateValidator=" + Integer.toHexString(System.identityHashCode(this.certificateValidator)); + return "|certificateValidator=" + this.certificateValidator.getClass().getName() + "@" + + Integer.toHexString(System.identityHashCode(this.certificateValidator)); } if (this.trustListDirectory != null && !this.trustListDirectory.isEmpty()) { return "|trustListDirectory=" + this.trustListDirectory; diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java index 33cd020e2db98..5e3d92a0459c4 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java @@ -20,10 +20,13 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.camel.component.milo.client.MiloClientConfiguration; +import org.eclipse.milo.opcua.stack.core.StatusCodes; +import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.security.CertificateQuarantine; import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; import org.eclipse.milo.opcua.stack.core.security.DefaultClientCertificateValidator; @@ -31,6 +34,7 @@ import org.eclipse.milo.opcua.stack.core.security.FileBasedTrustListManager; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; +import org.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +44,31 @@ */ final class ServerCertificateTrust implements Closeable { + /** + * The checks of the trust list validator besides the chain of trust. As OPC UA requires, a certificate issued by a + * CA is refused when the trust list holds no revocation list of that CA. A self-signed certificate of the trust + * list is not checked for revocation. + */ + static final Set TRUST_LIST_CHECKS + = Set.of(ValidationCheck.APPLICATION_URI, ValidationCheck.VALIDITY, ValidationCheck.REVOCATION); + + private static final Set CERTIFICATE_STATUS_CODES = Set.of( + StatusCodes.Bad_SecurityChecksFailed, + StatusCodes.Bad_CertificateInvalid, + StatusCodes.Bad_CertificateUntrusted, + StatusCodes.Bad_CertificateTimeInvalid, + StatusCodes.Bad_CertificateIssuerTimeInvalid, + StatusCodes.Bad_CertificateHostNameInvalid, + StatusCodes.Bad_CertificateUriInvalid, + StatusCodes.Bad_CertificateUseNotAllowed, + StatusCodes.Bad_CertificateIssuerUseNotAllowed, + StatusCodes.Bad_CertificateRevocationUnknown, + StatusCodes.Bad_CertificateIssuerRevocationUnknown, + StatusCodes.Bad_CertificateRevoked, + StatusCodes.Bad_CertificateIssuerRevoked, + StatusCodes.Bad_CertificateChainIncomplete, + StatusCodes.Bad_CertificatePolicyCheckFailed); + private static final Logger LOG = LoggerFactory.getLogger(ServerCertificateTrust.class); private final MiloClientConfiguration configuration; @@ -57,11 +86,11 @@ final class ServerCertificateTrust implements Closeable { /** * Returns the validator for the certificate of the server that provides the given endpoint. * - * @param endpoint the endpoint selected from the discovery result, may be {@code null} - * @return the validator to use for the secure channel, the session and the identity token - * @throws IllegalStateException if the endpoint uses a security policy other than None and no way to validate the - * server certificate is configured - * @throws IOException if the trust list directory cannot be read or created + * @param endpoint the endpoint selected from the discovery result, may be {@code null} + * @return the validator to use for the secure channel, the session and the identity token + * @throws NotConfiguredException if the endpoint uses a security policy other than None and no way to validate the + * server certificate is configured + * @throws IOException if the trust list directory cannot be read or created */ CertificateValidator validatorFor(final EndpointDescription endpoint) throws IOException { if (this.configuration.getCertificateValidator() != null) { @@ -81,12 +110,12 @@ CertificateValidator validatorFor(final EndpointDescription endpoint) throws IOE } if (isSecured(endpoint)) { - throw new IllegalStateException( + throw new NotConfiguredException( "The certificate of the OPC UA server cannot be validated: endpoint " + endpoint.getEndpointUrl() - + " uses security policy " + endpoint.getSecurityPolicyUri() - + ", but neither trustListDirectory nor certificateValidator is configured." - + " Configure one of them, or set trustAllCertificates=true to accept any" - + " server certificate (insecure)."); + + " uses security policy " + endpoint.getSecurityPolicyUri() + + ", but neither trustListDirectory nor certificateValidator is configured." + + " Configure one of them, or set trustAllCertificates=true to accept any" + + " server certificate (insecure)."); } // SecurityPolicy None: the server is not authenticated in any case, and its certificate is at most used to @@ -101,6 +130,28 @@ static boolean isSecured(final EndpointDescription endpoint) { return endpoint != null && !SecurityPolicy.None.getUri().equals(endpoint.getSecurityPolicyUri()); } + /** + * Finds the reason of a connection failure that a retry does not fix by itself: a missing trust configuration, or a + * certificate or security check that failed, on the client or on the server side. + * + * @param failure the failure of a connection attempt + * @return the exception that describes the reason, or {@code null} for any other failure + */ + static Throwable findCertificateFailure(final Throwable failure) { + Throwable current = failure; + for (int depth = 0; current != null && depth < 16; depth++) { + if (current instanceof NotConfiguredException) { + return current; + } + if (current instanceof UaException uaException + && CERTIFICATE_STATUS_CODES.contains(uaException.getStatusCode().getValue())) { + return current; + } + current = current.getCause(); + } + return null; + } + private CertificateValidator trustListValidator(final Path directory) throws IOException { this.lock.lock(); try { @@ -119,7 +170,7 @@ private CertificateValidator trustListValidator(final Path directory) throws IOE } this.trustListManager = manager; - this.trustListValidator = new DefaultClientCertificateValidator(manager, quarantine); + this.trustListValidator = new DefaultClientCertificateValidator(manager, TRUST_LIST_CHECKS, quarantine); } return this.trustListValidator; } finally { @@ -160,4 +211,17 @@ public void close() throws IOException { manager.close(); } } + + /** + * The endpoint uses a security policy other than None, but no way to validate the server certificate is configured. + * Retrying does not help until the configuration changes. + */ + static final class NotConfiguredException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + NotConfiguredException(final String message) { + super(message); + } + } } diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java index a71c169aa212a..646e2554cfad2 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java @@ -590,6 +590,7 @@ nodeId, direction, NodeIds.References, includeSubTypes, uint(nodeClasses), private final Lock lock = new ReentrantLock(); private Connected connected; + private volatile Throwable lastConnectFailure; private boolean disposed; private Future reconnectJob; private final Map subscriptions = new HashMap<>(); @@ -626,6 +627,7 @@ private void handleConnectionFailure(final Throwable e) { this.connected.dispose(); this.connected = null; } + this.lastConnectFailure = e; // log @@ -675,6 +677,7 @@ private void performAndEvalConnect() { LOG.debug("Update state : {} -> {}", this.connected, connected); final Connected oldConnected = this.connected; this.connected = connected; + this.lastConnectFailure = null; if (oldConnected != null) { LOG.debug("Dispose old state"); @@ -690,11 +693,28 @@ private void performAndEvalConnect() { lock.unlock(); } } catch (final Exception e) { - LOG.info("Failed to connect", e); + this.lastConnectFailure = e; + logConnectFailure(e); triggerReconnect(false); } } + private static void logConnectFailure(final Exception e) { + final Throwable certificateFailure = ServerCertificateTrust.findCertificateFailure(e); + if (certificateFailure instanceof ServerCertificateTrust.NotConfiguredException) { + LOG.warn("Failed to connect: {}", certificateFailure.getMessage()); + LOG.debug("Failed to connect", e); + } else if (certificateFailure != null) { + // a certificate rejected by the trust list can be trusted later, so the client keeps retrying + // passed as text: SLF4J would log a trailing Throwable as the exception, with its stack trace + LOG.warn("Failed to connect, because a certificate or security check failed: {}", + String.valueOf(certificateFailure)); + LOG.debug("Failed to connect", e); + } else { + LOG.info("Failed to connect", e); + } + } + private Connected performConnect() throws Exception { // eval endpoint @@ -945,9 +965,10 @@ protected void whenConnected(final Worker worker) { } } - private static CompletableFuture newNotConnectedResult() { + private CompletableFuture newNotConnectedResult() { final CompletableFuture result = new CompletableFuture<>(); - result.completeExceptionally(new IllegalStateException("No connected")); + // the reason of the last failed connection attempt, if any, tells the caller why there is no connection + result.completeExceptionally(new IllegalStateException("No connected", this.lastConnectFailure)); return result; } diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java index 30337115bef48..98a822dd4085a 100644 --- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java @@ -28,6 +28,7 @@ import org.apache.camel.CamelContext; import org.apache.camel.EndpointInject; +import org.apache.camel.Exchange; import org.apache.camel.Produce; import org.apache.camel.ProducerTemplate; import org.apache.camel.RoutesBuilder; @@ -125,10 +126,8 @@ protected RoutesBuilder createRouteBuilder() { public void configure() { from(DIRECT_START).to(MILO_SERVER_ITEM); - from(resolve(MILO_CLIENT + "&trustListDirectory=RAW(" + pki.resolve("trusted") + ")")) - .to("mock:trustList"); - from(resolve(MILO_CLIENT + "&trustListDirectory=RAW(" + pki.resolve("empty") + ")")) - .to("mock:emptyTrustList"); + from(resolve(trustListClient("trusted"))).to("mock:trustList"); + from(resolve(trustListClient("empty"))).to("mock:emptyTrustList"); from(resolve(MILO_CLIENT)).to("mock:notConfigured"); from(resolve(MILO_CLIENT + "&trustAllCertificates=true")).to("mock:trustAll"); from(resolve(MILO_CLIENT + "&certificateValidator=#recordingValidator")).to("mock:certificateValidator"); @@ -156,6 +155,26 @@ void onlyClientsThatCanValidateTheServerCertificateConnect() throws Exception { await().atMost(10, TimeUnit.SECONDS).until(() -> countFiles(rejectedCerts) > 0); assertTrue(this.recordingValidator.validations.get() > 0, "The configured certificate validator was not used"); + + // a producer of a client that cannot connect gets the reason of the last connection attempt + assertConnectionFailure(MILO_CLIENT, "The certificate of the OPC UA server cannot be validated"); + assertConnectionFailure(trustListClient("empty"), "Bad_SecurityChecksFailed"); + } + + private void assertConnectionFailure(final String clientUri, final String expectedReason) { + final Exchange exchange = this.producer.send(resolve(clientUri + "&defaultAwaitWrites=true"), + e -> e.getMessage().setBody("Bar")); + + final StringBuilder causes = new StringBuilder(); + for (Throwable cause = exchange.getException(); cause != null; cause = cause.getCause()) { + causes.append(cause).append('\n'); + } + assertTrue(causes.indexOf("No connected") >= 0, causes::toString); + assertTrue(causes.indexOf(expectedReason) >= 0, causes::toString); + } + + private static String trustListClient(final String directory) { + return MILO_CLIENT + "&trustListDirectory=RAW(" + pki.resolve(directory) + ")"; } private static long countFiles(final Path directory) throws IOException { diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java index d9f33c6bc502e..a9a05de0c8e07 100644 --- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java @@ -16,15 +16,21 @@ */ package org.apache.camel.component.milo.client.internal; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPair; +import java.security.cert.X509CRL; import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import org.apache.camel.component.milo.call.TestCertificateFactory; import org.apache.camel.component.milo.client.MiloClientConfiguration; +import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; import org.eclipse.milo.opcua.stack.core.security.DefaultClientCertificateValidator; @@ -40,30 +46,34 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class ServerCertificateTrustTest { - private static X509Certificate serverCertificate; + private static X509Certificate selfSignedServerCertificate; + private static TestPki pki; @TempDir Path trustListDirectory; @BeforeAll - static void createServerCertificate() { + static void createCertificates() throws Exception { final TestCertificateFactory certificateFactory = new TestCertificateFactory(); final KeyPair keyPair = certificateFactory.createRsaSha256KeyPair(); - serverCertificate = certificateFactory.createRsaSha256CertificateChain(keyPair)[0]; + selfSignedServerCertificate = certificateFactory.createRsaSha256CertificateChain(keyPair)[0]; + pki = new TestPki("Camel Milo Test CA"); } @Test void securedEndpointWithoutTrustConfigurationIsRefused() { final ServerCertificateTrust trust = new ServerCertificateTrust(new MiloClientConfiguration()); - final IllegalStateException e - = assertThrows(IllegalStateException.class, () -> trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + final ServerCertificateTrust.NotConfiguredException e = assertThrows( + ServerCertificateTrust.NotConfiguredException.class, + () -> trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); assertTrue(e.getMessage().contains("trustListDirectory"), e.getMessage()); assertTrue(e.getMessage().contains("certificateValidator"), e.getMessage()); @@ -86,7 +96,7 @@ void trustAllCertificatesAcceptsAnyServerCertificate() throws Exception { final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); - assertDoesNotThrow(() -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + assertDoesNotThrow(() -> validator.validateCertificateChain(List.of(selfSignedServerCertificate), null, null)); } @Test @@ -105,17 +115,14 @@ void certificateValidatorIsUsedBeforeOtherOptions() throws Exception { @Test void trustListAcceptsTrustedServerCertificate() throws Exception { - final Path trustedCerts = Files.createDirectories(this.trustListDirectory.resolve("trusted").resolve("certs")); - Files.write(trustedCerts.resolve("server.der"), serverCertificate.getEncoded()); - - final MiloClientConfiguration configuration = new MiloClientConfiguration(); - configuration.setTrustListDirectory(this.trustListDirectory.toString()); + trust(selfSignedServerCertificate); - try (ServerCertificateTrust trust = new ServerCertificateTrust(configuration)) { + try (ServerCertificateTrust trust = trustListTrust()) { final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); assertInstanceOf(DefaultClientCertificateValidator.class, validator); - assertDoesNotThrow(() -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + assertDoesNotThrow( + () -> validator.validateCertificateChain(List.of(selfSignedServerCertificate), null, null)); // a reconnect uses the same trust list assertSame(validator, trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); } @@ -123,28 +130,79 @@ void trustListAcceptsTrustedServerCertificate() throws Exception { @Test void trustListRejectsAndKeepsUntrustedServerCertificate() throws Exception { - final MiloClientConfiguration configuration = new MiloClientConfiguration(); - configuration.setTrustListDirectory(this.trustListDirectory.toString()); + // the trust list holds a CA that did not issue the server certificate + trust(new TestPki("Another Test CA").certificate()); - try (ServerCertificateTrust trust = new ServerCertificateTrust(configuration)) { + try (ServerCertificateTrust trust = trustListTrust()) { final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); - assertThrows(UaException.class, - () -> validator.validateCertificateChain(List.of(serverCertificate), null, null)); + // Milo reports a certificate without a chain up to a trusted certificate as a failed security check + final UaException e = assertThrows(UaException.class, + () -> validator.validateCertificateChain(List.of(selfSignedServerCertificate), null, null)); + assertEquals(StatusCodes.Bad_SecurityChecksFailed, e.getStatusCode().getValue(), e.toString()); } assertTrue(Files.isDirectory(this.trustListDirectory.resolve("trusted").resolve("certs"))); assertTrue(Files.isDirectory(this.trustListDirectory.resolve("issuer").resolve("certs"))); - try (Stream rejected = Files.list(this.trustListDirectory.resolve("rejected").resolve("certs"))) { - assertEquals(1, rejected.count()); + assertEquals(1, countRejected()); + } + + @Test + void trustListAcceptsServerCertificateOfCaWithRevocationList() throws Exception { + final X509Certificate serverCertificate = validServerCertificate(); + trust(pki.certificate()); + trustRevocationList(pki.revocationList()); + + try (ServerCertificateTrust trust = trustListTrust()) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + assertDoesNotThrow(() -> validator.validateCertificateChain( + List.of(serverCertificate, pki.certificate()), TestPki.SERVER_APPLICATION_URI, null)); } } + @Test + void trustListRefusesServerCertificateOfCaWithoutRevocationList() throws Exception { + final X509Certificate serverCertificate = validServerCertificate(); + trust(pki.certificate()); + + assertRefused(List.of(serverCertificate, pki.certificate()), TestPki.SERVER_APPLICATION_URI, + StatusCodes.Bad_CertificateRevocationUnknown); + } + + @Test + void trustListRefusesRevokedServerCertificate() throws Exception { + final X509Certificate serverCertificate = validServerCertificate(); + trust(pki.certificate()); + trustRevocationList(pki.revocationList(serverCertificate)); + + assertRefused(List.of(serverCertificate, pki.certificate()), TestPki.SERVER_APPLICATION_URI, + StatusCodes.Bad_CertificateRevoked); + } + + @Test + void trustListRefusesExpiredServerCertificate() throws Exception { + final Instant now = Instant.now(); + final X509Certificate serverCertificate = TestPki.selfSignedServerCertificate( + now.minus(2, ChronoUnit.DAYS), now.minus(1, ChronoUnit.DAYS)); + trust(serverCertificate); + + assertRefused(List.of(serverCertificate), TestPki.SERVER_APPLICATION_URI, StatusCodes.Bad_CertificateTimeInvalid); + } + + @Test + void trustListRefusesServerCertificateOfAnotherApplication() throws Exception { + final Instant now = Instant.now(); + final X509Certificate serverCertificate = TestPki.selfSignedServerCertificate( + now.minus(1, ChronoUnit.HOURS), now.plus(1, ChronoUnit.HOURS)); + trust(serverCertificate); + + assertRefused(List.of(serverCertificate), "urn:camel:milo:test:other", StatusCodes.Bad_CertificateUriInvalid); + } + @Test void closedTrustListIsNotReused() throws Exception { - final MiloClientConfiguration configuration = new MiloClientConfiguration(); - configuration.setTrustListDirectory(this.trustListDirectory.toString()); - final ServerCertificateTrust trust = new ServerCertificateTrust(configuration); + final ServerCertificateTrust trust = trustListTrust(); trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); trust.close(); @@ -152,6 +210,66 @@ void closedTrustListIsNotReused() throws Exception { assertThrows(IllegalStateException.class, () -> trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); } + @Test + void certificateFailuresAreFoundInTheCauseChain() { + final ServerCertificateTrust.NotConfiguredException notConfigured + = new ServerCertificateTrust.NotConfiguredException("not configured"); + final UaException untrusted = new UaException(StatusCodes.Bad_CertificateUntrusted, "untrusted"); + + assertSame(notConfigured, + ServerCertificateTrust.findCertificateFailure(new IllegalStateException("wrapped", notConfigured))); + assertSame(untrusted, ServerCertificateTrust.findCertificateFailure(new ExecutionException(untrusted))); + assertNull(ServerCertificateTrust.findCertificateFailure(new UaException(StatusCodes.Bad_Timeout, "timeout"))); + assertNull(ServerCertificateTrust.findCertificateFailure(new IOException("connection refused"))); + } + + private void assertRefused( + final List certificateChain, final String applicationUri, final long statusCode) + throws Exception { + try (ServerCertificateTrust trust = trustListTrust()) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + final UaException e = assertThrows(UaException.class, + () -> validator.validateCertificateChain(certificateChain, applicationUri, null)); + assertEquals(statusCode, e.getStatusCode().getValue(), e.toString()); + } + // only a certificate that does not chain up to a trusted certificate is kept for review + assertEquals(0, countRejected()); + } + + private static X509Certificate validServerCertificate() throws Exception { + final Instant now = Instant.now(); + return pki.issueServerCertificate(now.minus(1, ChronoUnit.HOURS), now.plus(1, ChronoUnit.HOURS)); + } + + private ServerCertificateTrust trustListTrust() { + final MiloClientConfiguration configuration = new MiloClientConfiguration(); + configuration.setTrustListDirectory(this.trustListDirectory.toString()); + return new ServerCertificateTrust(configuration); + } + + private void trust(final X509Certificate certificate) throws Exception { + final Path trustedCerts = Files.createDirectories(this.trustListDirectory.resolve("trusted").resolve("certs")); + Files.write(trustedCerts.resolve(certificate.getSerialNumber() + "-" + + Integer.toHexString(certificate.hashCode()) + ".der"), + certificate.getEncoded()); + } + + private void trustRevocationList(final X509CRL revocationList) throws Exception { + final Path trustedCrl = Files.createDirectories(this.trustListDirectory.resolve("trusted").resolve("crl")); + Files.write(trustedCrl.resolve("ca.crl"), revocationList.getEncoded()); + } + + private long countRejected() throws IOException { + final Path rejectedCerts = this.trustListDirectory.resolve("rejected").resolve("certs"); + if (!Files.isDirectory(rejectedCerts)) { + return 0; + } + try (Stream rejected = Files.list(rejectedCerts)) { + return rejected.count(); + } + } + private static EndpointDescription endpoint(final SecurityPolicy securityPolicy) { final MessageSecurityMode securityMode = securityPolicy == SecurityPolicy.None ? MessageSecurityMode.None : MessageSecurityMode.SignAndEncrypt; diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/TestPki.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/TestPki.java new file mode 100644 index 0000000000000..0a0cca2805707 --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/TestPki.java @@ -0,0 +1,145 @@ +/* + * 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.camel.component.milo.client.internal; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.CRLReason; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/** + * A certificate authority for tests, which issues server certificates and certificate revocation lists. + */ +final class TestPki { + + static final String SERVER_APPLICATION_URI = "urn:camel:milo:test:server"; + + private final X500Name name; + private final KeyPair keyPair; + private final X509Certificate certificate; + private long serialNumber; + + TestPki(final String commonName) throws Exception { + this.name = new X500Name("CN=" + commonName); + this.keyPair = newKeyPair(); + + final Instant now = Instant.now(); + final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + this.name, nextSerialNumber(), Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(1, ChronoUnit.DAYS)), this.name, this.keyPair.getPublic()); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); + this.certificate = sign(builder); + } + + X509Certificate certificate() { + return this.certificate; + } + + /** + * Issues a certificate for a server with the application URI {@link #SERVER_APPLICATION_URI}. + */ + X509Certificate issueServerCertificate(final Instant notBefore, final Instant notAfter) throws Exception { + final X509v3CertificateBuilder builder = serverCertificateBuilder( + this.name, nextSerialNumber(), notBefore, notAfter, newKeyPair().getPublic(), 0); + return sign(builder); + } + + /** + * Creates a self-signed certificate for a server with the application URI {@link #SERVER_APPLICATION_URI}. As OPC + * UA requires for a self-signed application certificate, it may sign certificates, since it is its own issuer. + */ + static X509Certificate selfSignedServerCertificate(final Instant notBefore, final Instant notAfter) throws Exception { + final KeyPair keyPair = newKeyPair(); + final X509v3CertificateBuilder builder = serverCertificateBuilder( + new X500Name("CN=Camel Milo Test Server"), BigInteger.ONE, notBefore, notAfter, keyPair.getPublic(), + KeyUsage.keyCertSign); + return new JcaX509CertificateConverter().getCertificate( + builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()))); + } + + private static X509v3CertificateBuilder serverCertificateBuilder( + final X500Name issuer, final BigInteger serialNumber, final Instant notBefore, final Instant notAfter, + final PublicKey publicKey, final int additionalKeyUsage) + throws Exception { + final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + issuer, serialNumber, Date.from(notBefore), Date.from(notAfter), + new X500Name("CN=Camel Milo Test Server"), publicKey); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + builder.addExtension(Extension.keyUsage, true, new KeyUsage( + KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment + | additionalKeyUsage)); + builder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames( + new GeneralName[] { + new GeneralName(GeneralName.uniformResourceIdentifier, SERVER_APPLICATION_URI), + new GeneralName(GeneralName.dNSName, "localhost") })); + return builder; + } + + /** + * Issues a certificate revocation list that revokes the given certificates. + */ + X509CRL revocationList(final X509Certificate... revoked) throws Exception { + final Instant now = Instant.now(); + final X509v2CRLBuilder builder = new X509v2CRLBuilder(this.name, Date.from(now.minus(1, ChronoUnit.HOURS))); + builder.setNextUpdate(Date.from(now.plus(1, ChronoUnit.DAYS))); + for (final X509Certificate certificate : revoked) { + builder.addCRLEntry(certificate.getSerialNumber(), Date.from(now.minus(1, ChronoUnit.HOURS)), + CRLReason.keyCompromise); + } + return new JcaX509CRLConverter().getCRL(builder.build(signer())); + } + + private X509Certificate sign(final X509v3CertificateBuilder builder) throws Exception { + return new JcaX509CertificateConverter().getCertificate(builder.build(signer())); + } + + private ContentSigner signer() throws Exception { + return new JcaContentSignerBuilder("SHA256withRSA").build(this.keyPair.getPrivate()); + } + + private BigInteger nextSerialNumber() { + this.serialNumber++; + return BigInteger.valueOf(this.serialNumber); + } + + private static KeyPair newKeyPair() throws Exception { + final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + return generator.generateKeyPair(); + } +} diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java index ee2fe9ba66c33..3e97cae4e5ac9 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java @@ -465,7 +465,8 @@ default MiloBrowseComponentBuilder sessionTimeout(java.lang.Long sessionTimeout) /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * <code>org.eclipse.milo.opcua.stack.core.security.CertificateValidator</code> type. @@ -523,9 +524,11 @@ default MiloBrowseComponentBuilder trustAllCertificates(boolean trustAllCertific * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java index c2104d30a32fb..443c136377101 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java @@ -493,7 +493,8 @@ default MiloClientComponentBuilder sessionTimeout(java.lang.Long sessionTimeout) /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * <code>org.eclipse.milo.opcua.stack.core.security.CertificateValidator</code> type. @@ -551,9 +552,11 @@ default MiloClientComponentBuilder trustAllCertificates(boolean trustAllCertific * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java index 6d0c0156403bd..b3f3aaf37ff00 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java @@ -688,7 +688,8 @@ default MiloBrowseEndpointBuilder sessionTimeout(String sessionTimeout) { /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -705,7 +706,8 @@ default MiloBrowseEndpointBuilder certificateValidator(org.eclipse.milo.opcua.st /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option will be converted to a * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -777,9 +779,11 @@ default MiloBrowseEndpointBuilder trustAllCertificates(String trustAllCertificat * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: java.lang.String type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java index a1a5f2e05824c..8024d425c26a5 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java @@ -712,7 +712,8 @@ default MiloClientEndpointConsumerBuilder sessionTimeout(String sessionTimeout) /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -729,7 +730,8 @@ default MiloClientEndpointConsumerBuilder certificateValidator(org.eclipse.milo. /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option will be converted to a * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -801,9 +803,11 @@ default MiloClientEndpointConsumerBuilder trustAllCertificates(String trustAllCe * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: java.lang.String type. * @@ -1640,7 +1644,8 @@ default MiloClientEndpointProducerBuilder sessionTimeout(String sessionTimeout) /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -1657,7 +1662,8 @@ default MiloClientEndpointProducerBuilder certificateValidator(org.eclipse.milo. /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option will be converted to a * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -1729,9 +1735,11 @@ default MiloClientEndpointProducerBuilder trustAllCertificates(String trustAllCe * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: java.lang.String type. * @@ -2497,7 +2505,8 @@ default MiloClientEndpointBuilder sessionTimeout(String sessionTimeout) { /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option is a: * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -2514,7 +2523,8 @@ default MiloClientEndpointBuilder certificateValidator(org.eclipse.milo.opcua.st /** * A custom validator for the certificate of the OPC UA server. When * set, it is used instead of the trust list configured with - * trustListDirectory. + * trustListDirectory. Endpoints share a connection only when they use + * the same validator instance. * * The option will be converted to a * org.eclipse.milo.opcua.stack.core.security.CertificateValidator type. @@ -2586,9 +2596,11 @@ default MiloClientEndpointBuilder trustAllCertificates(String trustAllCertificat * certificates in trusted/certs, CRLs of trusted certificates in * trusted/crl, certificates of issuing CAs in issuer/certs and their * CRLs in issuer/crl. The directories are created if they do not exist. - * A server certificate that fails validation is written to - * rejected/certs, from where it can be moved to trusted/certs once it - * has been checked. + * Besides the chain of trust, the validity period, the revocation by a + * CRL of the trust list and the application URI of the server + * certificate are checked. A server certificate that does not chain up + * to a trusted certificate is written to rejected/certs, from where it + * can be moved to trusted/certs once it has been checked. * * The option is a: java.lang.String type. * From c9826900d9f5c71433a149a1162bcef7ff0a5f91 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Thu, 15 Oct 2026 12:41:34 +0200 Subject: [PATCH 3/5] CAMEL-25645: camel-milo - document the revocation list requirement of the trust list A server certificate issued by a CA is refused with Bad_CertificateRevocationUnknown when the trust list holds no revocation list of that CA. The component documentation, the option description and the upgrade guide said the opposite, and now describe this requirement. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Andrea Cosentino --- .../camel-milo/src/main/docs/milo-client-component.adoc | 5 +++-- .../camel/component/milo/client/MiloClientConfiguration.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc b/components/camel-milo/src/main/docs/milo-client-component.adoc index 29627f9960d3b..7b2f7948d3f49 100644 --- a/components/camel-milo/src/main/docs/milo-client-component.adoc +++ b/components/camel-milo/src/main/docs/milo-client-component.adoc @@ -224,8 +224,9 @@ With `trustListDirectory`, the client refuses a server certificate that: * does not chain up to a certificate in `trusted/certs`, possibly through the certificates in `issuer/certs`, * is outside its validity period, -* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. When the trust list has no revocation -list for an issuer, the revocation status of the certificates it issued is not checked, +* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. A certificate issued by a CA is also +refused, with `Bad_CertificateRevocationUnknown`, when the trust list holds no revocation list of that CA. An empty +CRL signed by the CA is enough. A self-signed certificate in `trusted/certs` is not checked for revocation, * or holds an application URI that differs from the one the server announces in its endpoint description. A failed host name, key usage or extended key usage check is only logged. A certificate that does not chain up to a diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java index 2354caa26b4c9..9303da7d1fad6 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java @@ -465,8 +465,9 @@ public String getPassword() { * Eclipse Milo: trusted certificates in trusted/certs, CRLs of trusted certificates in trusted/crl, certificates of * issuing CAs in issuer/certs and their CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application - * URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate - * is written to rejected/certs, from where it can be moved to trusted/certs once it has been checked. + * URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no + * CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to trusted/certs once it has been checked. */ public void setTrustListDirectory(final String trustListDirectory) { this.trustListDirectory = trustListDirectory; From 22534c9a51108064413e67a36889f729c80ea354 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Thu, 15 Oct 2026 13:34:52 +0200 Subject: [PATCH 4/5] CAMEL-25645: camel-milo - adapt the backport to camel-4.22.x and regenerate On camel-4.22.x the connection cache id does not contain the requested publishing interval, so the cache id test expects the endpoint URI alone. The generated files are regenerated for this branch. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Andrea Cosentino --- .../camel/catalog/components/milo-browse.json | 4 ++-- .../camel/catalog/components/milo-client.json | 14 +++++++---- .../catalog/docs/milo-client-component.adoc | 5 ++-- .../client/MiloClientEndpointUriFactory.java | 5 +++- .../component/milo/browse/milo-browse.json | 4 ++-- .../component/milo/client/milo-client.json | 14 +++++++---- .../client/MiloClientConfigurationTest.java | 2 +- .../MiloBrowseComponentBuilderFactory.java | 8 ++++--- .../MiloClientComponentBuilderFactory.java | 8 ++++--- .../dsl/MiloBrowseEndpointBuilderFactory.java | 8 ++++--- .../dsl/MiloClientEndpointBuilderFactory.java | 24 ++++++++++++------- 11 files changed, 62 insertions(+), 34 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json index 21f0f5d866875..3592fe710be86 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-browse.json @@ -52,7 +52,7 @@ "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { @@ -93,7 +93,7 @@ "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json index 04edc8db1b511..b5d359ecca0d4 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json @@ -50,8 +50,11 @@ "requestTimeout": { "index": 23, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 24, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 25, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 27, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 26, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, + "password": { "index": 27, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 28, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 29, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 30, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The node ids.", "constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" }, @@ -93,7 +96,10 @@ "requestTimeout": { "index": 32, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 33, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 34, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 35, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 36, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 35, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, + "password": { "index": 36, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 37, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 38, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 39, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc index 29627f9960d3b..7b2f7948d3f49 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc @@ -224,8 +224,9 @@ With `trustListDirectory`, the client refuses a server certificate that: * does not chain up to a certificate in `trusted/certs`, possibly through the certificates in `issuer/certs`, * is outside its validity period, -* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. When the trust list has no revocation -list for an issuer, the revocation status of the certificates it issued is not checked, +* is revoked by a certificate revocation list in `trusted/crl` or `issuer/crl`. A certificate issued by a CA is also +refused, with `Bad_CertificateRevocationUnknown`, when the trust list holds no revocation list of that CA. An empty +CRL signed by the CA is enough. A self-signed certificate in `trusted/certs` is not checked for revocation, * or holds an application URI that differs from the one the server announces in its endpoint description. A failed host name, key usage or extended key usage check is only logged. A certificate that does not chain up to a diff --git a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java index 575bfabdd7981..d1f9fce776b1e 100644 --- a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java +++ b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java @@ -24,11 +24,12 @@ public class MiloClientEndpointUriFactory extends org.apache.camel.support.compo private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(37); + Set props = new HashSet<>(40); props.add("allowedSecurityPolicies"); props.add("applicationName"); props.add("applicationUri"); props.add("bridgeErrorHandler"); + props.add("certificateValidator"); props.add("channelLifetime"); props.add("clientId"); props.add("dataChangeFilterDeadbandType"); @@ -61,6 +62,8 @@ public class MiloClientEndpointUriFactory extends org.apache.camel.support.compo props.add("samplingInterval"); props.add("sessionName"); props.add("sessionTimeout"); + props.add("trustAllCertificates"); + props.add("trustListDirectory"); props.add("username"); PROPERTY_NAMES = Collections.unmodifiableSet(props); Set secretProps = new HashSet<>(3); diff --git a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json index 21f0f5d866875..3592fe710be86 100644 --- a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json +++ b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/browse/milo-browse.json @@ -52,7 +52,7 @@ "certificateValidator": { "index": 25, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 27, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 28, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 29, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { @@ -93,7 +93,7 @@ "certificateValidator": { "index": 31, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, "password": { "index": 32, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, "trustAllCertificates": { "index": 33, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, - "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "trustListDirectory": { "index": 34, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, "username": { "index": 35, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json index 04edc8db1b511..b5d359ecca0d4 100644 --- a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json +++ b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json @@ -50,8 +50,11 @@ "requestTimeout": { "index": 23, "kind": "property", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 24, "kind": "property", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 25, "kind": "property", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 26, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 27, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 26, "kind": "property", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, + "password": { "index": 27, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 28, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 29, "kind": "property", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 30, "kind": "property", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } }, "headers": { "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The node ids.", "constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" }, @@ -93,7 +96,10 @@ "requestTimeout": { "index": 32, "kind": "parameter", "displayName": "Request Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Request timeout in milliseconds" }, "sessionName": { "index": 33, "kind": "parameter", "displayName": "Session Name", "group": "client", "label": "client", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session name" }, "sessionTimeout": { "index": 34, "kind": "parameter", "displayName": "Session Timeout", "group": "client", "label": "client", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Session timeout in milliseconds" }, - "password": { "index": 35, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, - "username": { "index": 36, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } + "certificateValidator": { "index": 35, "kind": "parameter", "displayName": "Certificate Validator", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.eclipse.milo.opcua.stack.core.security.CertificateValidator", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "A custom validator for the certificate of the OPC UA server. When set, it is used instead of the trust list configured with trustListDirectory. Endpoints share a connection only when they use the same validator instance." }, + "password": { "index": 36, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The password for authentication. Use this instead of embedding credentials in the endpoint URI when the password contains special characters (such as {code }, {code \/}, {code }, {code &})." }, + "trustAllCertificates": { "index": 37, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "Whether to accept any certificate presented by the OPC UA server. This is insecure: the connection is encrypted, but the client does not authenticate the server it talks to. It only applies when neither certificateValidator nor trustListDirectory is configured." }, + "trustListDirectory": { "index": 38, "kind": "parameter", "displayName": "Trust List Directory", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The directory holding the trust list used to validate the certificate of the OPC UA server, in the layout used by Eclipse Milo: trusted certificates in trusted\/certs, CRLs of trusted certificates in trusted\/crl, certificates of issuing CAs in issuer\/certs and their CRLs in issuer\/crl. The directories are created if they do not exist. Besides the chain of trust, the validity period, the revocation by a CRL of the trust list and the application URI of the server certificate are checked. A certificate issued by a CA is refused when the trust list holds no CRL of that CA; an empty CRL is enough. A server certificate that does not chain up to a trusted certificate is written to rejected\/certs, from where it can be moved to trusted\/certs once it has been checked." }, + "username": { "index": 39, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The username for authentication. Use this instead of embedding credentials in the endpoint URI when the username contains special characters (such as {code }, {code \/}, {code }, {code &})." } } } diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java index 68d0a6a7c4157..5f8edb88c1eef 100644 --- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/MiloClientConfigurationTest.java @@ -33,7 +33,7 @@ class MiloClientConfigurationTest { @Test void cacheIdIsUnchangedWithoutServerCertificateTrustSettings() { - assertEquals(ENDPOINT_URI + "|1000.0", configuration().toCacheId()); + assertEquals(ENDPOINT_URI, configuration().toCacheId()); } @Test diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java index 3e97cae4e5ac9..832c3c13b8cb1 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloBrowseComponentBuilderFactory.java @@ -526,9 +526,11 @@ default MiloBrowseComponentBuilder trustAllCertificates(boolean trustAllCertific * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java index 443c136377101..6fd19bb55fc64 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MiloClientComponentBuilderFactory.java @@ -554,9 +554,11 @@ default MiloClientComponentBuilder trustAllCertificates(boolean trustAllCertific * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java index b3f3aaf37ff00..2371bf904c42a 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloBrowseEndpointBuilderFactory.java @@ -781,9 +781,11 @@ default MiloBrowseEndpointBuilder trustAllCertificates(String trustAllCertificat * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: java.lang.String type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java index 8024d425c26a5..631ec20ffa6cc 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java @@ -805,9 +805,11 @@ default MiloClientEndpointConsumerBuilder trustAllCertificates(String trustAllCe * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: java.lang.String type. * @@ -1737,9 +1739,11 @@ default MiloClientEndpointProducerBuilder trustAllCertificates(String trustAllCe * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: java.lang.String type. * @@ -2598,9 +2602,11 @@ default MiloClientEndpointBuilder trustAllCertificates(String trustAllCertificat * CRLs in issuer/crl. The directories are created if they do not exist. * Besides the chain of trust, the validity period, the revocation by a * CRL of the trust list and the application URI of the server - * certificate are checked. A server certificate that does not chain up - * to a trusted certificate is written to rejected/certs, from where it - * can be moved to trusted/certs once it has been checked. + * certificate are checked. A certificate issued by a CA is refused when + * the trust list holds no CRL of that CA; an empty CRL is enough. A + * server certificate that does not chain up to a trusted certificate is + * written to rejected/certs, from where it can be moved to + * trusted/certs once it has been checked. * * The option is a: java.lang.String type. * From b57441de94f5f36f150502fda8125b751063f8e2 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sat, 17 Oct 2026 15:30:01 +0200 Subject: [PATCH 5/5] CAMEL-25645: camel-milo - ServerCertificateValidationTest writes the value before the consumers start (#27999) Since CAMEL-25626 a consumer also delivers a null value with a Bad status, so a client that connected before the test wrote Foo received the initial Bad value of the item as well, and mock:trustAll got 2 messages. The two changes were merged on main 26 seconds apart and neither PR build ran with the other. The consumer routes now start after the value is written, as MonitorItemTest does since CAMEL-25626, so Foo is the first notification of each client. Signed-off-by: Claus Ibsen Co-authored-by: Claude Opus 5.5 (1M context) (cherry picked from commit eb14b16f700600fe244755a5960a293398942773) --- .../milo/ServerCertificateValidationTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java index 98a822dd4085a..12352d91f9c6f 100644 --- a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java @@ -126,11 +126,13 @@ protected RoutesBuilder createRouteBuilder() { public void configure() { from(DIRECT_START).to(MILO_SERVER_ITEM); - from(resolve(trustListClient("trusted"))).to("mock:trustList"); - from(resolve(trustListClient("empty"))).to("mock:emptyTrustList"); - from(resolve(MILO_CLIENT)).to("mock:notConfigured"); - from(resolve(MILO_CLIENT + "&trustAllCertificates=true")).to("mock:trustAll"); - from(resolve(MILO_CLIENT + "&certificateValidator=#recordingValidator")).to("mock:certificateValidator"); + // the consumers deliver the current value while they start, so the test starts these routes + from(resolve(trustListClient("trusted"))).autoStartup(false).to("mock:trustList"); + from(resolve(trustListClient("empty"))).autoStartup(false).to("mock:emptyTrustList"); + from(resolve(MILO_CLIENT)).autoStartup(false).to("mock:notConfigured"); + from(resolve(MILO_CLIENT + "&trustAllCertificates=true")).autoStartup(false).to("mock:trustAll"); + from(resolve(MILO_CLIENT + "&certificateValidator=#recordingValidator")).autoStartup(false) + .to("mock:certificateValidator"); } }; } @@ -146,7 +148,10 @@ void onlyClientsThatCanValidateTheServerCertificateConnect() throws Exception { this.notConfiguredEndpoint.setExpectedCount(0); this.notConfiguredEndpoint.setSleepForEmptyTest(5_000); + // the value is written before the consumers start, so it is the first notification of each client that + // connects (and not the initial Bad value of the item) this.producer.sendBody("Foo"); + context.getRouteController().startAllRoutes(); MockEndpoint.assertIsSatisfied(context);