This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new bd4c4af906 feat: Extend certificate information shown in UI (#3739)
bd4c4af906 is described below
commit bd4c4af90604e6e1b7be5c6ebf3cafbe6054d7df
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Aug 21 07:22:15 2025 +0200
feat: Extend certificate information shown in UI (#3739)
* feat: Extend certificate information shown in UI
* Fix rat
---
.../security/CompositeCertificateValidator.java | 16 +-
.../connectors/opcua/utils/OpcUaUtils.java | 16 +-
.../streampipes/model/opcua/Certificate.java | 75 +++++++
.../model/opcua/CertificateBuilder.java | 247 +++++++++++++++++++++
.../src/lib/model/gen/streampipes-model.ts | 20 +-
.../certificate-details-dialog.component.html | 25 ++-
.../certificate-details-dialog.component.ts | 4 +
7 files changed, 377 insertions(+), 26 deletions(-)
diff --git
a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java
b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java
index 98dc2f5d73..c88392c729 100644
---
a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java
+++
b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/config/security/CompositeCertificateValidator.java
@@ -20,7 +20,7 @@ package
org.apache.streampipes.extensions.connectors.opcua.config.security;
import org.apache.streampipes.client.api.IStreamPipesClient;
import org.apache.streampipes.extensions.connectors.opcua.utils.OpcUaUtils;
-import org.apache.streampipes.model.opcua.Certificate;
+import org.apache.streampipes.model.opcua.CertificateBuilder;
import org.apache.streampipes.model.opcua.CertificateState;
import org.eclipse.milo.opcua.stack.client.security.ClientCertificateValidator;
@@ -36,7 +36,6 @@ import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Base64;
import java.util.List;
import java.util.stream.Stream;
@@ -138,18 +137,7 @@ public class CompositeCertificateValidator implements
ClientCertificateValidator
private void sendToCore(X509Certificate cert) {
try {
- var certificate = new Certificate(
- cert.getSubjectX500Principal().getName(),
- cert.getIssuerX500Principal().getName(),
- cert.getSerialNumber().toString(),
- cert.getNotBefore().toString(),
- cert.getNotAfter().toString(),
- cert.getSigAlgName(),
- cert.getPublicKey().getAlgorithm(),
- Base64.getEncoder().encodeToString(cert.getEncoded()),
- CertificateState.REJECTED
- );
-
+ var certificate = CertificateBuilder.fromX509(cert,
CertificateState.REJECTED);
streamPipesClient.customRequest().sendPost(OpcUaUtils.getCoreCertificatePath(),
certificate);
} catch (Exception ex) {
LOG.error("Failed to report rejected certificate to API", ex);
diff --git
a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java
b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java
index b5e3b65176..df09ed3050 100644
---
a/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java
+++
b/streampipes-extensions/streampipes-connectors-opcua/src/main/java/org/apache/streampipes/extensions/connectors/opcua/utils/OpcUaUtils.java
@@ -81,7 +81,6 @@ public class OpcUaUtils {
}
var opcUaConfig =
SpOpcUaConfigExtractor.extractSharedConfig(parameterExtractor, new
OpcUaAdapterConfig(), client);
-
try {
var connectedClient = clientProvider.getClient(opcUaConfig);
OpcUaNodeBrowser nodeBrowser =
@@ -100,13 +99,14 @@ public class OpcUaUtils {
);
}
-
return config;
} catch (UaException e) {
throw new
SpConfigurationException(ExceptionMessageExtractor.getDescription(e), e);
} catch (ExecutionException | InterruptedException | URISyntaxException e)
{
if (e instanceof ExecutionException &&
isCertificateException((ExecutionException) e)) {
- throw new SpConfigurationException("The provided certificate could not
be trusted. Administrators can accept this certificate in the settings.", e);
+ throw new SpConfigurationException(
+ makeExceptionMessage((ExecutionException) e)
+ );
} else {
throw new SpConfigurationException("Could not connect to the OPC UA
server with the provided settings", e);
}
@@ -154,4 +154,14 @@ public class OpcUaUtils {
return false;
}
+ private static String makeExceptionMessage(ExecutionException e) {
+ StringBuilder message = new StringBuilder(
+ "The provided certificate could not be trusted. Administrators can
accept this certificate in the settings. "
+ );
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ message.append("Reason: ").append(cause.getMessage());
+ }
+ return message.toString();
+ }
}
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java
index df1a65b4b0..85ba67e008 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/Certificate.java
@@ -24,6 +24,7 @@ import org.apache.streampipes.model.shared.api.Storable;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.google.gson.annotations.SerializedName;
+import java.util.List;
import java.util.Objects;
@TsModel
@@ -44,9 +45,15 @@ public final class Certificate implements Storable {
private String notAfter;
private String sigAlgName;
private String algorithm;
+ private String basicConstraints;
+ private List<String> keyUsages;
+ private List<String> extendedKeyUsages;
+ private List<String> subjectAlternativeNames;
private String certificateDerBase64;
+
private CertificateState state;
+
public Certificate() {
}
@@ -126,6 +133,74 @@ public final class Certificate implements Storable {
return state;
}
+ public void setSubjectDn(String subjectDn) {
+ this.subjectDn = subjectDn;
+ }
+
+ public void setIssuerDn(String issuerDn) {
+ this.issuerDn = issuerDn;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
+
+ public void setNotBefore(String notBefore) {
+ this.notBefore = notBefore;
+ }
+
+ public void setNotAfter(String notAfter) {
+ this.notAfter = notAfter;
+ }
+
+ public void setSigAlgName(String sigAlgName) {
+ this.sigAlgName = sigAlgName;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public String getBasicConstraints() {
+ return basicConstraints;
+ }
+
+ public void setBasicConstraints(String basicConstraints) {
+ this.basicConstraints = basicConstraints;
+ }
+
+ public List<String> getKeyUsages() {
+ return keyUsages;
+ }
+
+ public void setKeyUsages(List<String> keyUsages) {
+ this.keyUsages = keyUsages;
+ }
+
+ public List<String> getExtendedKeyUsages() {
+ return extendedKeyUsages;
+ }
+
+ public void setExtendedKeyUsages(List<String> extendedKeyUsages) {
+ this.extendedKeyUsages = extendedKeyUsages;
+ }
+
+ public List<String> getSubjectAlternativeNames() {
+ return subjectAlternativeNames;
+ }
+
+ public void setSubjectAlternativeNames(List<String> subjectAlternativeNames)
{
+ this.subjectAlternativeNames = subjectAlternativeNames;
+ }
+
+ public void setCertificateDerBase64(String certificateDerBase64) {
+ this.certificateDerBase64 = certificateDerBase64;
+ }
+
+ public void setState(CertificateState state) {
+ this.state = state;
+ }
+
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java
new file mode 100644
index 0000000000..716e264157
--- /dev/null
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/opcua/CertificateBuilder.java
@@ -0,0 +1,247 @@
+/*
+ * 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.streampipes.model.opcua;
+
+import javax.security.auth.x500.X500Principal;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public final class CertificateBuilder {
+
+ // fluent setters
+ public CertificateBuilder subjectDn(String v) {
+ cert.setSubjectDn(v);
+ return this;
+ }
+
+ public CertificateBuilder issuerDn(String v) {
+ cert.setIssuerDn(v);
+ return this;
+ }
+
+ public CertificateBuilder serialNumber(String v) {
+ cert.setSerialNumber(v);
+ return this;
+ }
+
+ public CertificateBuilder notBefore(String v) {
+ cert.setNotBefore(v);
+ return this;
+ }
+
+ public CertificateBuilder notAfter(String v) {
+ cert.setNotAfter(v);
+ return this;
+ }
+
+ public CertificateBuilder sigAlgName(String v) {
+ cert.setSigAlgName(v);
+ return this;
+ }
+
+ public CertificateBuilder algorithm(String v) {
+ cert.setAlgorithm(v);
+ return this;
+ }
+
+ public CertificateBuilder basicConstraints(String v) {
+ cert.setBasicConstraints(v);
+ return this;
+ }
+
+ public CertificateBuilder keyUsages(List<String> v) {
+ cert.setKeyUsages(v);
+ return this;
+ }
+
+ public CertificateBuilder extendedKeyUsages(List<String> v) {
+ cert.setExtendedKeyUsages(v);
+ return this;
+ }
+
+ public CertificateBuilder subjectAlternativeNames(List<String> v) {
+ cert.setSubjectAlternativeNames(v);
+ return this;
+ }
+
+ public CertificateBuilder certificateDerBase64(String v) {
+ cert.setCertificateDerBase64(v);
+ return this;
+ }
+
+ private final Certificate cert;
+
+ private CertificateBuilder() {
+ cert = new Certificate();
+ }
+
+ public Certificate build() {
+ return cert;
+ }
+
+ public static Certificate fromX509(X509Certificate cert, CertificateState
state) {
+ Objects.requireNonNull(cert, "cert");
+ var b = new CertificateBuilder();
+
+ var certificate = b
+ .subjectDn(name(cert.getSubjectX500Principal()))
+ .issuerDn(name(cert.getIssuerX500Principal()))
+ .serialNumber(hex(cert.getSerialNumber()))
+ .notBefore(cert.getNotBefore().toString())
+ .notAfter(cert.getNotAfter().toString())
+ .sigAlgName(cert.getSigAlgName())
+ .algorithm(describePublicKey(cert.getPublicKey()))
+ .basicConstraints(describeBasicConstraints(cert.getBasicConstraints()))
+ .keyUsages(describeKeyUsage(cert.getKeyUsage()))
+
.extendedKeyUsages(describeExtendedKeyUsage(safe(cert::getExtendedKeyUsage)))
+
.subjectAlternativeNames(describeSANs(safe(cert::getSubjectAlternativeNames)))
+ .certificateDerBase64(base64(safe(cert::getEncoded)))
+ .build();
+
+ certificate.setState(state);
+
+ return certificate;
+ }
+
+ private static String name(X500Principal p) {
+ return p == null ? "" : p.getName(X500Principal.RFC2253);
+ }
+
+ private static String hex(BigInteger bi) {
+ return bi == null ? "" : bi.toString(16).toUpperCase(Locale.ROOT);
+ }
+
+ private static String base64(byte[] bytes) {
+ return bytes == null ? "" : Base64.getEncoder().encodeToString(bytes);
+ }
+
+ private static <T> T safe(SupplierWithThrow<T> s) {
+ try {
+ return s.get();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @FunctionalInterface
+ private interface SupplierWithThrow<T> {
+ T get() throws Exception;
+ }
+
+ private static String describePublicKey(PublicKey pk) {
+ if (pk == null) {
+ return "";
+ }
+ if (pk instanceof RSAPublicKey rsa) {
+ return "RSA (" + rsa.getModulus().bitLength() + " bits)";
+ }
+ if (pk instanceof ECPublicKey ec) {
+ return "EC (" + ec.getParams().getCurve().getField().getFieldSize() + "
bits)";
+ }
+ if (pk instanceof DSAPublicKey dsa && dsa.getParams() != null) {
+ return "DSA (" + dsa.getParams().getP().bitLength() + " bits)";
+ }
+ return pk.getAlgorithm();
+ }
+
+ private static String describeBasicConstraints(int bc) {
+ if (bc < 0) {
+ return "End-entity (no CA)";
+ }
+ if (bc == Integer.MAX_VALUE) {
+ return "CA: true, pathLen: unlimited";
+ }
+ return "CA: true, pathLen: " + bc;
+ }
+
+ private static List<String> describeKeyUsage(boolean[] ku) {
+ if (ku == null) {
+ return List.of();
+ }
+ String[] names = {"digitalSignature", "contentCommitment",
"keyEncipherment", "dataEncipherment",
+ "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly",
"decipherOnly"};
+ List<String> out = new ArrayList<>();
+ for (int i = 0; i < ku.length && i < names.length; i++) {
+ if (ku[i]) {
+ out.add(names[i]);
+ }
+ }
+ return out;
+ }
+
+ private static final Map<String, String> EKU_KNOWN = Map.ofEntries(
+ Map.entry("2.5.29.37.0", "anyExtendedKeyUsage"),
+ Map.entry("1.3.6.1.5.5.7.3.1", "serverAuth"),
+ Map.entry("1.3.6.1.5.5.7.3.2", "clientAuth"),
+ Map.entry("1.3.6.1.5.5.7.3.3", "codeSigning"),
+ Map.entry("1.3.6.1.5.5.7.3.4", "emailProtection"),
+ Map.entry("1.3.6.1.5.5.7.3.8", "timeStamping"),
+ Map.entry("1.3.6.1.5.5.7.3.9", "OCSPSigning")
+ );
+
+ private static List<String> describeExtendedKeyUsage(List<String> oids) {
+ if (oids == null) {
+ return List.of();
+ }
+ return oids.stream().map(oid -> EKU_KNOWN.getOrDefault(oid, "OID:" +
oid)).toList();
+ }
+
+ private static List<String> describeSANs(Collection<List<?>> sans) {
+ if (sans == null) {
+ return List.of();
+ }
+ List<String> out = new ArrayList<>();
+ for (List<?> entry : sans) {
+ if (entry == null || entry.size() < 2) {
+ continue;
+ }
+ int tag = (Integer) entry.get(0);
+ Object val = entry.get(1);
+ String label = switch (tag) {
+ case 1 -> "rfc822Name";
+ case 2 -> "DNS";
+ case 6 -> "URI";
+ case 7 -> "IP";
+ default -> "SAN(" + tag + ")";
+ };
+ if (tag == 7 && val instanceof byte[] bytes) {
+ try {
+ val = InetAddress.getByAddress(bytes).getHostAddress();
+ } catch (Exception ignored) {
+ }
+ }
+ out.add(label + ": " + val);
+ }
+ return out;
+ }
+}
+
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 3b1c5d8e9a..acc60b0040 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -20,7 +20,7 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-07-29
10:59:26.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-20
10:54:16.
export class NamedStreamPipesEntity implements Storable {
'@class':
@@ -672,15 +672,19 @@ export class CanvasPosition {
export class Certificate implements Storable {
algorithm: string;
+ basicConstraints: string;
certificateDerBase64: string;
elementId: string;
+ extendedKeyUsages: string[];
issuerDn: string;
+ keyUsages: string[];
notAfter: string;
notBefore: string;
rev: string;
serialNumber: string;
sigAlgName: string;
state: CertificateState;
+ subjectAlternativeNames: string[];
subjectDn: string;
static fromData(data: Certificate, target?: Certificate): Certificate {
@@ -689,15 +693,25 @@ export class Certificate implements Storable {
}
const instance = target || new Certificate();
instance.algorithm = data.algorithm;
+ instance.basicConstraints = data.basicConstraints;
instance.certificateDerBase64 = data.certificateDerBase64;
instance.elementId = data.elementId;
+ instance.extendedKeyUsages = __getCopyArrayFn(__identity<string>())(
+ data.extendedKeyUsages,
+ );
instance.issuerDn = data.issuerDn;
+ instance.keyUsages = __getCopyArrayFn(__identity<string>())(
+ data.keyUsages,
+ );
instance.notAfter = data.notAfter;
instance.notBefore = data.notBefore;
instance.rev = data.rev;
instance.serialNumber = data.serialNumber;
instance.sigAlgName = data.sigAlgName;
instance.state = data.state;
+ instance.subjectAlternativeNames = __getCopyArrayFn(
+ __identity<string>(),
+ )(data.subjectAlternativeNames);
instance.subjectDn = data.subjectDn;
return instance;
}
@@ -1189,8 +1203,6 @@ export class DashboardModel implements Storable,
SpResource {
export class DataExplorerWidgetModel extends DashboardEntity {
baseAppearanceConfig: { [index: string]: any };
dataConfig: { [index: string]: any };
- measureName: string;
- pipelineId: string;
timeSettings: { [index: string]: any };
visualizationConfig: { [index: string]: any };
widgetId: string;
@@ -1211,8 +1223,6 @@ export class DataExplorerWidgetModel extends
DashboardEntity {
instance.dataConfig = __getCopyObjectFn(__identity<any>())(
data.dataConfig,
);
- instance.measureName = data.measureName;
- instance.pipelineId = data.pipelineId;
instance.timeSettings = __getCopyObjectFn(__identity<any>())(
data.timeSettings,
);
diff --git
a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
index dd835d6cf3..44bd4f2ecf 100644
---
a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
+++
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.html
@@ -26,10 +26,27 @@
key !== 'certificateDerBase64'
) {
<div fxLayout="row" fxLayoutGap="10px" class="p-5">
- <span fxFlex="30"
- ><b>{{ key }}</b></span
- >
- <span fxFlex="70">{{ certificate[key] }}</span>
+ <div fxFlex="30">
+ <b>{{ key }}</b>
+ </div>
+ <div fxFlex="70">
+ @if (isArray(certificate[key])) {
+ <div fxLayout="row wrap" fxLayoutGap="5px">
+ @for (
+ item of certificate[key];
+ track $index
+ ) {
+ <sp-label
+ size="small"
+ [labelText]="item"
+ class="mb-10"
+ ></sp-label>
+ }
+ </div>
+ } @else {
+ <small>{{ certificate[key] }}</small>
+ }
+ </div>
</div>
}
}
diff --git
a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
index 1cf2e9a616..16c6b495d6 100644
---
a/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
+++
b/ui/src/app/configuration/dialog/certificate-details/certificate-details-dialog.component.ts
@@ -31,6 +31,10 @@ export class CertificateDetailsDialogComponent {
@Input()
certificate: Certificate;
+ isArray(value: any): boolean {
+ return Array.isArray(value);
+ }
+
close(): void {
this.dialogRef.close();
}