Skip to content

Commit fb262c5

Browse files
feat(#3710): Centrally manage OPC-UA certificates (#3720)
* feat(#3710): Add initial support for centrally managed OPC-UA certificates * Modify imports * Add certificate details dialog, improve exception messages * Add missing headers * Fix checkstyle
1 parent dd1da18 commit fb262c5

File tree

28 files changed

+977
-159
lines changed

28 files changed

+977
-159
lines changed

streampipes-client-api/src/main/java/org/apache/streampipes/client/api/ICustomRequestApi.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.apache.streampipes.client.api;
2020

21+
import java.util.List;
2122
import java.util.Map;
2223

2324
public interface ICustomRequestApi {
@@ -26,4 +27,6 @@ public interface ICustomRequestApi {
2627
<T> T sendGet(String apiPath, Class<T> responseClass);
2728

2829
<T> T sendGet(String apiPath, Map<String, String> queryParameters, Class<T> responseClass);
30+
31+
<T> List<T> getList(String apiPath, Class<T> response);
2932
}

streampipes-client/src/main/java/org/apache/streampipes/client/api/AbstractClientApi.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.streampipes.client.http.PostRequestWithoutPayloadResponse;
2626
import org.apache.streampipes.client.http.PutRequest;
2727
import org.apache.streampipes.client.model.StreamPipesClientConfig;
28+
import org.apache.streampipes.client.serializer.ListSerializer;
2829
import org.apache.streampipes.client.serializer.ObjectSerializer;
2930
import org.apache.streampipes.client.serializer.Serializer;
3031
import org.apache.streampipes.client.util.StreamPipesApiPath;
@@ -33,6 +34,7 @@
3334

3435
import org.apache.http.HttpStatus;
3536

37+
import java.util.List;
3638
import java.util.Optional;
3739

3840
public class AbstractClientApi {
@@ -86,6 +88,11 @@ protected <T> T getSingle(StreamPipesApiPath apiPath, Class<T> targetClass) thro
8688
return new GetRequest<>(clientConfig, apiPath, targetClass, serializer).executeRequest();
8789
}
8890

91+
protected <T> List<T> getList(StreamPipesApiPath apiPath, Class<T> targetClass) throws SpRuntimeException {
92+
ListSerializer<Void, T> serializer = new ListSerializer<>();
93+
return new GetRequest<>(clientConfig, apiPath, targetClass, serializer).executeRequest();
94+
}
95+
8996
protected <T> Optional<T> getSingleOpt(StreamPipesApiPath apiPath,
9097
Class<T> targetClass) throws SpRuntimeException {
9198
try {

streampipes-client/src/main/java/org/apache/streampipes/client/api/CustomRequestApi.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.streampipes.client.model.StreamPipesClientConfig;
2121
import org.apache.streampipes.client.util.StreamPipesApiPath;
2222

23+
import java.util.List;
2324
import java.util.Map;
2425

2526
public class CustomRequestApi extends AbstractClientApi implements ICustomRequestApi {
@@ -46,4 +47,11 @@ public <T> T sendGet(String apiPath, Map<String, String> queryParameters, Class<
4647
responseClass);
4748
}
4849

50+
@Override
51+
public <T> List<T> getList(String apiPath, Class<T> responseClass) {
52+
return getList(
53+
StreamPipesApiPath.fromStreamPipesBasePath(apiPath), responseClass
54+
);
55+
}
56+
4957
}

streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaAdapter.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ public void onAdapterStarted(IAdapterParameterExtractor extractor,
204204
IEventCollector collector,
205205
IAdapterRuntimeContext adapterRuntimeContext) throws AdapterException {
206206
this.opcUaAdapterConfig =
207-
SpOpcUaConfigExtractor.extractAdapterConfig(extractor.getStaticPropertyExtractor());
207+
SpOpcUaConfigExtractor.extractAdapterConfig(
208+
extractor.getStaticPropertyExtractor(),
209+
adapterRuntimeContext.getStreamPipesClient()
210+
);
208211
this.collector = collector;
209212
this.prepareAdapter(extractor);
210213
this.numberOfEventProperties =
@@ -252,6 +255,6 @@ public IAdapterConfiguration declareConfig() {
252255
@Override
253256
public GuessSchema onSchemaRequested(IAdapterParameterExtractor extractor,
254257
IAdapterGuessSchemaContext adapterGuessSchemaContext) throws AdapterException {
255-
return new OpcUaSchemaProvider().getSchema(clientProvider, extractor);
258+
return new OpcUaSchemaProvider().getSchema(clientProvider, extractor, adapterGuessSchemaContext.getStreamPipesClient());
256259
}
257260
}

streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/adapter/OpcUaSchemaProvider.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.apache.streampipes.extensions.connectors.opcua.adapter;
2020

21+
import org.apache.streampipes.client.api.IStreamPipesClient;
2122
import org.apache.streampipes.commons.exceptions.connect.AdapterException;
2223
import org.apache.streampipes.commons.exceptions.connect.ParseException;
2324
import org.apache.streampipes.extensions.api.extractor.IAdapterParameterExtractor;
@@ -51,7 +52,8 @@ public class OpcUaSchemaProvider {
5152
* @throws ParseException
5253
*/
5354
public GuessSchema getSchema(OpcUaClientProvider clientProvider,
54-
IAdapterParameterExtractor extractor)
55+
IAdapterParameterExtractor extractor,
56+
IStreamPipesClient streamPipesClient)
5557
throws AdapterException, ParseException {
5658
var builder = GuessSchemaBuilder.create();
5759
EventSchema eventSchema = new EventSchema();
@@ -60,7 +62,8 @@ public GuessSchema getSchema(OpcUaClientProvider clientProvider,
6062
List<EventProperty> allProperties = new ArrayList<>();
6163

6264
var opcUaConfig = SpOpcUaConfigExtractor.extractAdapterConfig(
63-
extractor.getStaticPropertyExtractor()
65+
extractor.getStaticPropertyExtractor(),
66+
streamPipesClient
6467
);
6568
try {
6669
var connectedClient = clientProvider.getClient(opcUaConfig);

streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/SpOpcUaConfigExtractor.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.apache.streampipes.extensions.connectors.opcua.config;
2020

21+
import org.apache.streampipes.client.api.IStreamPipesClient;
2122
import org.apache.streampipes.extensions.api.extractor.IParameterExtractor;
2223
import org.apache.streampipes.extensions.api.extractor.IStaticPropertyExtractor;
2324
import org.apache.streampipes.extensions.connectors.opcua.config.identity.AnonymousIdentityConfig;
@@ -53,8 +54,9 @@ public class SpOpcUaConfigExtractor {
5354
* @param extractor extractor for user inputs
5455
* @return {@link OpcUaAdapterConfig} instance based on information from {@code extractor}
5556
*/
56-
public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor extractor) {
57-
var config = extractSharedConfig(extractor, new OpcUaAdapterConfig());
57+
public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor extractor,
58+
IStreamPipesClient streamPipesClient) {
59+
var config = extractSharedConfig(extractor, new OpcUaAdapterConfig(), streamPipesClient);
5860
boolean usePullMode = extractor.selectedAlternativeInternalId(ADAPTER_TYPE.name())
5961
.equals(PULL_MODE.name());
6062

@@ -78,12 +80,14 @@ public static OpcUaAdapterConfig extractAdapterConfig(IStaticPropertyExtractor e
7880
return config;
7981
}
8082

81-
public static OpcUaConfig extractSinkConfig(IParameterExtractor extractor) {
82-
return extractSharedConfig(extractor, new OpcUaConfig());
83+
public static OpcUaConfig extractSinkConfig(IParameterExtractor extractor,
84+
IStreamPipesClient streamPipesClient) {
85+
return extractSharedConfig(extractor, new OpcUaConfig(), streamPipesClient);
8386
}
8487

8588
public static <T extends OpcUaConfig> T extractSharedConfig(IParameterExtractor extractor,
86-
T config) {
89+
T config,
90+
IStreamPipesClient streamPipesClient) {
8791

8892
String selectedAlternativeConnection =
8993
extractor.selectedAlternativeInternalId(OPC_HOST_OR_URL.name());
@@ -103,9 +107,13 @@ public static <T extends OpcUaConfig> T extractSharedConfig(IParameterExtractor
103107
SharedUserConfiguration.SECURITY_POLICY,
104108
String.class
105109
);
106-
config.setSecurityConfig(new SecurityConfig(
107-
MessageSecurityMode.valueOf(selectedSecurityMode),
108-
SecurityPolicy.valueOf(selectedSecurityPolicy)));
110+
config.setSecurityConfig(
111+
new SecurityConfig(
112+
MessageSecurityMode.valueOf(selectedSecurityMode),
113+
SecurityPolicy.valueOf(selectedSecurityPolicy),
114+
streamPipesClient
115+
)
116+
);
109117

110118
boolean useURL = selectedAlternativeConnection.equals(OPC_URL.name());
111119
if (useURL) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.streampipes.extensions.connectors.opcua.config.security;
20+
21+
import org.apache.streampipes.client.api.IStreamPipesClient;
22+
import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaUtils;
23+
import org.apache.streampipes.model.opcua.Certificate;
24+
import org.apache.streampipes.model.opcua.CertificateState;
25+
26+
import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator;
27+
import org.eclipse.milo.opcua.stack.core.StatusCodes;
28+
import org.eclipse.milo.opcua.stack.core.UaException;
29+
import org.eclipse.milo.opcua.stack.core.security.TrustListManager;
30+
import org.eclipse.milo.opcua.stack.core.util.validation.CertificateValidationUtil;
31+
import org.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
import java.security.cert.PKIXCertPathBuilderResult;
36+
import java.security.cert.X509CRL;
37+
import java.security.cert.X509Certificate;
38+
import java.util.ArrayList;
39+
import java.util.Base64;
40+
import java.util.List;
41+
import java.util.stream.Stream;
42+
43+
public class CompositeCertificateValidator implements ClientCertificateValidator {
44+
45+
private static final Logger LOG = LoggerFactory.getLogger(CompositeCertificateValidator.class);
46+
47+
public static final List<Long> REJECTED_STATUS_CODES = List.of(
48+
StatusCodes.Bad_CertificateChainIncomplete,
49+
StatusCodes.Bad_CertificateInvalid,
50+
StatusCodes.Bad_NoValidCertificates,
51+
StatusCodes.Bad_CertificateUntrusted,
52+
StatusCodes.Bad_CertificateUseNotAllowed,
53+
StatusCodes.Bad_SecurityChecksFailed
54+
);
55+
56+
private final TrustListManager trustListManager;
57+
private final List<X509Certificate> trustedCerts;
58+
private final List<ValidationCheck> validationChecks;
59+
private final IStreamPipesClient streamPipesClient;
60+
61+
public CompositeCertificateValidator(TrustListManager trustListManager,
62+
List<X509Certificate> trustedCerts,
63+
List<ValidationCheck> validationChecks,
64+
IStreamPipesClient streamPipesClient) {
65+
this.trustListManager = trustListManager;
66+
this.trustedCerts = trustedCerts;
67+
this.validationChecks = validationChecks;
68+
this.streamPipesClient = streamPipesClient;
69+
}
70+
71+
@Override
72+
public void validateCertificateChain(List<X509Certificate> certificateChain) throws UaException {
73+
PKIXCertPathBuilderResult certPathResult;
74+
75+
try {
76+
certPathResult = CertificateValidationUtil.buildTrustedCertPath(
77+
certificateChain,
78+
Stream.concat(trustListManager.getTrustedCertificates().stream(), trustedCerts.stream()).toList(),
79+
trustListManager.getIssuerCertificates()
80+
);
81+
} catch (UaException e) {
82+
if (isCertificateRejected(e.getStatusCode().getValue())) {
83+
sendToCore(certificateChain.get(0));
84+
}
85+
throw e;
86+
}
87+
88+
var crls = new ArrayList<X509CRL>();
89+
crls.addAll(trustListManager.getTrustedCrls());
90+
crls.addAll(trustListManager.getIssuerCrls());
91+
92+
CertificateValidationUtil.validateTrustedCertPath(
93+
certPathResult.getCertPath(),
94+
certPathResult.getTrustAnchor(),
95+
crls,
96+
ValidationCheck.NO_OPTIONAL_CHECKS,
97+
false
98+
);
99+
}
100+
101+
@Override
102+
public void validateCertificateChain(
103+
List<X509Certificate> certificateChain,
104+
String applicationUri,
105+
String... validHostNames
106+
) throws UaException {
107+
108+
validateCertificateChain(certificateChain);
109+
110+
X509Certificate certificate = certificateChain.get(0);
111+
112+
try {
113+
CertificateValidationUtil.checkApplicationUri(certificate, applicationUri);
114+
} catch (UaException e) {
115+
if (validationChecks.contains(ValidationCheck.APPLICATION_URI)) {
116+
throw e;
117+
} else {
118+
LOG.warn(
119+
"check suppressed: certificate failed application uri check: {} != {}",
120+
applicationUri, CertificateValidationUtil.getSubjectAltNameUri(certificate)
121+
);
122+
}
123+
}
124+
125+
try {
126+
CertificateValidationUtil.checkHostnameOrIpAddress(certificate, validHostNames);
127+
} catch (UaException e) {
128+
if (validationChecks.contains(ValidationCheck.HOSTNAME)) {
129+
throw e;
130+
} else {
131+
LOG.warn(
132+
"check suppressed: certificate failed hostname check: {}",
133+
certificate.getSubjectX500Principal().getName()
134+
);
135+
}
136+
}
137+
}
138+
139+
private void sendToCore(X509Certificate cert) {
140+
try {
141+
var certificate = new Certificate(
142+
cert.getSubjectX500Principal().getName(),
143+
cert.getIssuerX500Principal().getName(),
144+
cert.getSerialNumber().toString(),
145+
cert.getNotBefore().toString(),
146+
cert.getNotAfter().toString(),
147+
cert.getSigAlgName(),
148+
cert.getPublicKey().getAlgorithm(),
149+
Base64.getEncoder().encodeToString(cert.getEncoded()),
150+
CertificateState.REJECTED
151+
);
152+
153+
streamPipesClient.customRequest().sendPost(OpcUaUtils.getCoreCertificatePath(), certificate);
154+
} catch (Exception ex) {
155+
LOG.error("Failed to report rejected certificate to API", ex);
156+
}
157+
}
158+
159+
private boolean isCertificateRejected(long statusCode) {
160+
return REJECTED_STATUS_CODES.contains(statusCode);
161+
}
162+
}

0 commit comments

Comments
 (0)