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();
     }

Reply via email to