This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch extend-certificate-infos in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 0d8e95d5eb43e167f507b07a247290844ac60c40 Author: Dominik Riemer <[email protected]> AuthorDate: Wed Aug 20 13:51:13 2025 +0200 feat: Extend certificate information shown in UI --- .../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 | 39 ++-- .../certificate-details-dialog.component.html | 25 ++- .../certificate-details-dialog.component.ts | 4 + 7 files changed, 377 insertions(+), 45 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..2d8a85b07a 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 @@ -1,26 +1,7 @@ -/* - * 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. - */ - /* 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 +653,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 +674,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 +1184,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 +1204,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(); }
