From 9652445cc653331b3b763d39e840d0179f8dcf61 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Thu, 15 Oct 2026 13:55:54 +0200 Subject: [PATCH 1/2] 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. trustAllCertificates accepts any server certificate. The trust list checks the application URI, the validity period and the revocation status, and a certificate issued by a CA needs a revocation list of that CA. The same validator checks the certificate that the user identity token is encrypted with. 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. Backport of #27824, adapted to camel-4.18.x: @UriParam has no security attribute on this branch, so trustAllCertificates is not marked insecure:ssl; the user name and password only come from the endpoint URI; SubscriptionManager uses synchronized blocks; the connection cache id does not contain the requested publishing interval. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Andrea Cosentino --- .../camel/catalog/components/milo-browse.json | 10 +- .../camel/catalog/components/milo-client.json | 10 +- .../browse/MiloBrowseComponentConfigurer.java | 18 ++ .../browse/MiloBrowseEndpointConfigurer.java | 18 ++ .../browse/MiloBrowseEndpointUriFactory.java | 5 +- .../client/MiloClientComponentConfigurer.java | 18 ++ .../client/MiloClientEndpointConfigurer.java | 18 ++ .../client/MiloClientEndpointUriFactory.java | 5 +- .../component/milo/browse/milo-browse.json | 10 +- .../component/milo/client/milo-client.json | 10 +- .../src/main/docs/milo-browse-component.adoc | 2 +- .../src/main/docs/milo-client-component.adoc | 52 +++- .../milo/client/MiloClientConfiguration.java | 76 ++++- .../internal/ServerCertificateTrust.java | 227 ++++++++++++++ .../client/internal/SubscriptionManager.java | 43 ++- .../milo/ServerCertificateValidationTest.java | 208 +++++++++++++ .../client/MiloClientConfigurationTest.java | 83 +++++ .../internal/ServerCertificateTrustTest.java | 280 +++++++++++++++++ .../milo/client/internal/TestPki.java | 145 +++++++++ .../MiloBrowseComponentBuilderFactory.java | 68 ++++ .../MiloClientComponentBuilderFactory.java | 68 ++++ .../dsl/MiloBrowseEndpointBuilderFactory.java | 97 ++++++ .../dsl/MiloClientEndpointBuilderFactory.java | 291 ++++++++++++++++++ 23 files changed, 1744 insertions(+), 18 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 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 5994b4e3f3044..ccd89d0aff60b 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 @@ -48,7 +48,10 @@ "requestedPublishingInterval": { "index": 21, "kind": "property", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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. Endpoints share a connection only when they use the same validator instance." }, + "trustAllCertificates": { "index": 26, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 27, "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." } }, "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" } @@ -84,6 +87,9 @@ "requestedPublishingInterval": { "index": 27, "kind": "parameter", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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. Endpoints share a connection only when they use the same validator instance." }, + "trustAllCertificates": { "index": 32, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 33, "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." } } } 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 7ff0b561032f5..d411080554fa2 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 @@ -49,7 +49,10 @@ "requestedPublishingInterval": { "index": 22, "kind": "property", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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" }, + "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." }, + "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, "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 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." } }, "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" }, @@ -90,6 +93,9 @@ "requestedPublishingInterval": { "index": 31, "kind": "parameter", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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" }, + "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." }, + "trustAllCertificates": { "index": 36, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 37, "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." } } } 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 6354a32100ef7..180ba22add871 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": @@ -79,6 +81,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; default: return false; } } @@ -99,6 +105,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": @@ -140,6 +148,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; default: return null; } } @@ -156,6 +168,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": @@ -197,6 +211,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(); 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 708c394aee225..bc13d5bc47519 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": @@ -78,6 +80,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; default: return false; } } @@ -91,6 +97,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": @@ -140,6 +148,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; default: return null; } } @@ -154,6 +166,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": @@ -203,6 +217,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(); 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 634fb4f4e9fd8..c7588b62882cc 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 @@ -23,10 +23,11 @@ public class MiloBrowseEndpointUriFactory extends org.apache.camel.support.compo private static final Set SECRET_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(31); + Set props = new HashSet<>(34); props.add("allowedSecurityPolicies"); props.add("applicationName"); props.add("applicationUri"); + props.add("certificateValidator"); props.add("channelLifetime"); props.add("clientId"); props.add("depth"); @@ -55,6 +56,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"); PROPERTY_NAMES = Collections.unmodifiableSet(props); Set secretProps = new HashSet<>(2); secretProps.add("keyPassword"); 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 b28d4fe4fc692..0cc55b55642e0 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": @@ -81,6 +83,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; default: return false; } } @@ -103,6 +109,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": @@ -144,6 +152,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; default: return null; } } @@ -162,6 +174,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": @@ -203,6 +217,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(); 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 d5f5c46560e7c..77535b46e0590 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": @@ -89,6 +91,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; default: return false; } } @@ -104,6 +110,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": @@ -162,6 +170,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; default: return null; } } @@ -178,6 +190,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": @@ -236,6 +250,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(); default: return null; } } 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 620430d4a19cf..292289503ff34 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 @@ -23,11 +23,12 @@ public class MiloClientEndpointUriFactory extends org.apache.camel.support.compo private static final Set SECRET_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(35); + Set props = new HashSet<>(38); props.add("allowedSecurityPolicies"); props.add("applicationName"); props.add("applicationUri"); props.add("bridgeErrorHandler"); + props.add("certificateValidator"); props.add("channelLifetime"); props.add("clientId"); props.add("dataChangeFilterDeadbandType"); @@ -59,6 +60,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"); PROPERTY_NAMES = Collections.unmodifiableSet(props); Set secretProps = new HashSet<>(2); secretProps.add("keyPassword"); 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 5994b4e3f3044..ccd89d0aff60b 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 @@ -48,7 +48,10 @@ "requestedPublishingInterval": { "index": 21, "kind": "property", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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. Endpoints share a connection only when they use the same validator instance." }, + "trustAllCertificates": { "index": 26, "kind": "property", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 27, "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." } }, "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" } @@ -84,6 +87,9 @@ "requestedPublishingInterval": { "index": 27, "kind": "parameter", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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. Endpoints share a connection only when they use the same validator instance." }, + "trustAllCertificates": { "index": 32, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 33, "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." } } } 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 7ff0b561032f5..d411080554fa2 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 @@ -49,7 +49,10 @@ "requestedPublishingInterval": { "index": 22, "kind": "property", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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" }, + "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." }, + "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, "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 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." } }, "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" }, @@ -90,6 +93,9 @@ "requestedPublishingInterval": { "index": 31, "kind": "parameter", "displayName": "Requested Publishing Interval", "group": "client", "label": "client", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1_000.0", "configurationClass": "org.apache.camel.component.milo.client.MiloClientConfiguration", "configurationField": "configuration", "description": "The requested publishing interval in milliseconds" }, "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" } + "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" }, + "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." }, + "trustAllCertificates": { "index": 36, "kind": "parameter", "displayName": "Trust All Certificates", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "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": 37, "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." } } } 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 e1d0e997b38c4..38ffc11d309af 100644 --- a/components/camel-milo/src/main/docs/milo-browse-component.adoc +++ b/components/camel-milo/src/main/docs/milo-browse-component.adoc @@ -56,7 +56,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 aabf96826b4b5..8e8181dbd2604 100644 --- a/components/camel-milo/src/main/docs/milo-client-component.adoc +++ b/components/camel-milo/src/main/docs/milo-client-component.adoc @@ -155,13 +155,63 @@ 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. +* `certificateValidator`: a custom `org.eclipse.milo.opcua.stack.core.security.CertificateValidator`. It is used +instead of `trustListDirectory` when both are set. Endpoints share a connection only when they use the same validator +instance. + +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`. 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 +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. + +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 include::spring-boot:partial$starter.adoc[] 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 1cafb8cd5f2ce..60fa4a2e9d179 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; @@ -113,6 +114,15 @@ public class MiloClientConfiguration implements Cloneable { @UriParam(label = "client", defaultValue = "1_000.0") private Double requestedPublishingInterval = DEFAULT_REQUESTED_PUBLISHING_INTERVAL; + @UriParam(label = "security") + private String trustListDirectory; + + @UriParam(label = "security") + private CertificateValidator certificateValidator; + + @UriParam(label = "security") + private boolean trustAllCertificates; + public MiloClientConfiguration() { } @@ -138,6 +148,9 @@ public MiloClientConfiguration(final MiloClientConfiguration other) { this.overrideHost = other.overrideHost; this.overridePort = other.overridePort; this.requestedPublishingInterval = other.requestedPublishingInterval; + this.trustListDirectory = other.trustListDirectory; + this.certificateValidator = other.certificateValidator; + this.trustAllCertificates = other.trustAllCertificates; } /** @@ -419,17 +432,76 @@ public Double getRequestedPublishingInterval() { return requestedPublishingInterval; } + /** + * 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. + */ + 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. Endpoints share a connection only when they use the same validator instance. + */ + 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; + } + @Override public MiloClientConfiguration clone() { return new MiloClientConfiguration(this); } 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=" + this.certificateValidator.getClass().getName() + "@" + + 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..5e3d92a0459c4 --- /dev/null +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrust.java @@ -0,0 +1,227 @@ +/* + * 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.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; +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.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck; +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 { + + /** + * 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; + 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 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) { + 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 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)."); + } + + // 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()); + } + + /** + * 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 { + 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, TRUST_LIST_CHECKS, 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(); + } + } + + /** + * 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 f62e880e8d872..89dd3dfd40109 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; @@ -55,6 +56,7 @@ import org.eclipse.milo.opcua.stack.core.Identifiers; 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; @@ -582,8 +584,10 @@ nodeId, direction, Identifiers.References, includeSubTypes, uint(nodeClasses), private final MiloClientConfiguration configuration; private final ScheduledExecutorService executor; private final long reconnectTimeout; + private final ServerCertificateTrust serverCertificateTrust; private Connected connected; + private volatile Throwable lastConnectFailure; private boolean disposed; private Future reconnectJob; private final Map subscriptions = new HashMap<>(); @@ -594,6 +598,7 @@ public SubscriptionManager(final MiloClientConfiguration configuration, final Sc this.configuration = configuration; this.executor = executor; this.reconnectTimeout = reconnectTimeout; + this.serverCertificateTrust = new ServerCertificateTrust(configuration); connect(); } @@ -603,6 +608,7 @@ private synchronized void handleConnectionFailure(final Throwable e) { this.connected.dispose(); this.connected = null; } + this.lastConnectFailure = e; // log @@ -645,6 +651,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"); @@ -658,11 +665,28 @@ private void performAndEvalConnect() { } } } 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 @@ -697,6 +721,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<>(); @@ -707,7 +734,7 @@ private Connected performConnect() throws Exception { LOG.debug("Enable username/password provider: {}", creds[0]); } - providers.add(new UsernameProvider(creds[0], creds[1])); + providers.add(new UsernameProvider(creds[0], creds[1], certificateValidator)); } } @@ -716,6 +743,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( @@ -777,6 +805,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 synchronized void triggerReconnect(final boolean immediate) { @@ -883,9 +917,10 @@ protected synchronized 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 new file mode 100644 index 0000000000000..98a822dd4085a --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ServerCertificateValidationTest.java @@ -0,0 +1,208 @@ +/* + * 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.Exchange; +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(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"); + } + }; + } + + @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"); + + // 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 { + 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..5f8edb88c1eef --- /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, 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..a9a05de0c8e07 --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/client/internal/ServerCertificateTrustTest.java @@ -0,0 +1,280 @@ +/* + * 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.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; +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.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 selfSignedServerCertificate; + private static TestPki pki; + + @TempDir + Path trustListDirectory; + + @BeforeAll + static void createCertificates() throws Exception { + final TestCertificateFactory certificateFactory = new TestCertificateFactory(); + final KeyPair keyPair = certificateFactory.createRsaSha256KeyPair(); + selfSignedServerCertificate = certificateFactory.createRsaSha256CertificateChain(keyPair)[0]; + pki = new TestPki("Camel Milo Test CA"); + } + + @Test + void securedEndpointWithoutTrustConfigurationIsRefused() { + final ServerCertificateTrust trust = new ServerCertificateTrust(new MiloClientConfiguration()); + + 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()); + 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(selfSignedServerCertificate), 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 { + trust(selfSignedServerCertificate); + + try (ServerCertificateTrust trust = trustListTrust()) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + assertInstanceOf(DefaultClientCertificateValidator.class, validator); + assertDoesNotThrow( + () -> validator.validateCertificateChain(List.of(selfSignedServerCertificate), null, null)); + // a reconnect uses the same trust list + assertSame(validator, trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256))); + } + } + + @Test + void trustListRejectsAndKeepsUntrustedServerCertificate() throws Exception { + // the trust list holds a CA that did not issue the server certificate + trust(new TestPki("Another Test CA").certificate()); + + try (ServerCertificateTrust trust = trustListTrust()) { + final CertificateValidator validator = trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + // 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"))); + 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 ServerCertificateTrust trust = trustListTrust(); + trust.validatorFor(endpoint(SecurityPolicy.Basic256Sha256)); + + trust.close(); + + 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; + return new EndpointDescription( + "opc.tcp://localhost:4840", null, ByteString.NULL_VALUE, securityMode, securityPolicy.getUri(), null, null, + UByte.valueOf(0)); + } +} 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 27dba5d36c3a0..6fcb3df8cc214 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 @@ -461,6 +461,71 @@ default MiloBrowseComponentBuilder sessionTimeout(java.lang.Long 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. 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. + * + * 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; + } + + + /** + * 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. + * 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. + * + * 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; + } } class MiloBrowseComponentBuilderImpl @@ -507,6 +572,9 @@ 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 "trustAllCertificates": getOrCreateConfiguration((MiloBrowseComponent) component).setTrustAllCertificates((boolean) value); return true; + case "trustListDirectory": getOrCreateConfiguration((MiloBrowseComponent) component).setTrustListDirectory((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 d5ec2da4cd016..97f6ee997c66f 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 @@ -489,6 +489,71 @@ default MiloClientComponentBuilder sessionTimeout(java.lang.Long 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. 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. + * + * 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; + } + + + /** + * 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. + * 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. + * + * 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; + } } class MiloClientComponentBuilderImpl @@ -536,6 +601,9 @@ 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 "trustAllCertificates": getOrCreateConfiguration((MiloClientComponent) component).setTrustAllCertificates((boolean) value); return true; + case "trustListDirectory": getOrCreateConfiguration((MiloClientComponent) component).setTrustListDirectory((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 e9b7f9485ebf5..d0d7b56b3b58b 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,103 @@ 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. 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. + * + * 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. 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. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloBrowseEndpointBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + 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. + * 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. + * + * 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; + } } /** 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 2db17c5273820..e51293d9d97f1 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,103 @@ 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. 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. + * + * 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. 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. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointConsumerBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + 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. + * 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. + * + * 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; + } } /** @@ -1514,6 +1611,103 @@ 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. 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. + * + * 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. 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. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointProducerBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + 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. + * 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. + * + * 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; + } } /** @@ -2248,6 +2442,103 @@ 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. 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. + * + * 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. 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. + * + * Group: security + * + * @param certificateValidator the value to set + * @return the dsl builder + */ + default MiloClientEndpointBuilder certificateValidator(String certificateValidator) { + doSetProperty("certificateValidator", certificateValidator); + 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. + * 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. + * + * 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; + } } /** From bc1d46cee6266258a8f3f58c99cbddf1f0970558 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sat, 17 Oct 2026 15:30:01 +0200 Subject: [PATCH 2/2] 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);