From 3e1d310f59af883a625ce865d22625ebd756d4c7 Mon Sep 17 00:00:00 2001 From: kasemir Date: Fri, 31 Oct 2025 14:54:24 -0400 Subject: [PATCH 1/7] PVS Server: Check CERT:STATUS:... of client --- .../pva/client/ClientAuthentication.java | 15 +- .../epics/pva/client/ValidationHandler.java | 5 +- .../pva/common/CertificateStatusListener.java | 18 ++ .../pva/common/CertificateStatusMonitor.java | 208 ++++++++++++++++++ .../java/org/epics/pva/common/PVAAuth.java | 15 +- .../pva/server/ClientAuthentication.java | 104 +++++++++ .../pva/server/CreateChannelHandler.java | 2 +- .../java/org/epics/pva/server/PVAServer.java | 18 +- .../java/org/epics/pva/server/PutHandler.java | 3 +- .../java/org/epics/pva/server/ServerAuth.java | 141 ------------ .../epics/pva/server/ServerAuthorization.java | 24 ++ .../java/org/epics/pva/server/ServerPV.java | 54 ++++- .../epics/pva/server/ServerTCPHandler.java | 61 ++++- .../epics/pva/server/ValidationHandler.java | 9 +- 14 files changed, 484 insertions(+), 193 deletions(-) create mode 100644 core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java create mode 100644 core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java create mode 100644 core/pva/src/main/java/org/epics/pva/server/ClientAuthentication.java delete mode 100644 core/pva/src/main/java/org/epics/pva/server/ServerAuth.java create mode 100644 core/pva/src/main/java/org/epics/pva/server/ServerAuthorization.java diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java b/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java index ec54977c0c..cda3ff5497 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2023 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -23,7 +23,6 @@ /** PVA Client authentication modes * @author Kay Kasemir */ -@SuppressWarnings("nls") abstract class ClientAuthentication { /** @param buffer Buffer to which client's authentication info is added @@ -41,7 +40,7 @@ abstract class ClientAuthentication @Override public void encode(final ByteBuffer buffer) throws Exception { - PVAString.encodeString(PVAAuth.X509, buffer); + PVAString.encodeString(PVAAuth.x509.name(), buffer); // No detail because server already has name buffer.put(PVAFieldDesc.NULL_TYPE_CODE); } @@ -49,7 +48,7 @@ public void encode(final ByteBuffer buffer) throws Exception @Override public String toString() { - return PVAAuth.X509; + return PVAAuth.x509.name(); } }; @@ -60,7 +59,7 @@ public String toString() @Override public void encode(final ByteBuffer buffer) throws Exception { - PVAString.encodeString(PVAAuth.ANONYMOUS, buffer); + PVAString.encodeString(PVAAuth.anonymous.name(), buffer); // No detail because we're anonymous buffer.put(PVAFieldDesc.NULL_TYPE_CODE); } @@ -68,7 +67,7 @@ public void encode(final ByteBuffer buffer) throws Exception @Override public String toString() { - return PVAAuth.ANONYMOUS; + return PVAAuth.anonymous.name(); } }; @@ -102,7 +101,7 @@ private static class CAAuthentication extends ClientAuthentication @Override public void encode(final ByteBuffer buffer) throws Exception { - PVAString.encodeString(PVAAuth.CA, buffer); + PVAString.encodeString(PVAAuth.ca.name(), buffer); // Send identity detail identity.encodeType(buffer, new BitSet()); identity.encode(buffer); @@ -111,7 +110,7 @@ public void encode(final ByteBuffer buffer) throws Exception @Override public String toString() { - return "ca(" + user + "@" + host + ")"; + return PVAAuth.ca.name() + "(" + user + "@" + host + ")"; } } } diff --git a/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java b/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java index f57bc2afab..7def42c8c7 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java @@ -22,7 +22,6 @@ /** Handle a server's VALIDATION request * @author Kay Kasemir */ -@SuppressWarnings("nls") class ValidationHandler implements CommandHandler { @Override @@ -52,9 +51,9 @@ public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) t // Support "x509" or "ca" authorization, fall back to any-no-mouse final ClientAuthentication authentication; // Even if server suggests x509, check that we have a certificate with name - if (tcp.getClientX509Name() != null && auth.contains(PVAAuth.X509)) + if (tcp.getClientX509Name() != null && auth.contains(PVAAuth.x509.name())) authentication = ClientAuthentication.X509; - else if (auth.contains(PVAAuth.CA)) + else if (auth.contains(PVAAuth.ca.name())) authentication = ClientAuthentication.CA; else authentication = ClientAuthentication.Anonymous; diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java new file mode 100644 index 0000000000..fbdc8ced9d --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ + +package org.epics.pva.common; + +/** Listener to certificate status updates + * @author Kay Kasemir + */ +public interface CertificateStatusListener +{ + /** @param update Certificate status update */ + public void handleCertificateStatusUpdate(CertificateStatusMonitor.CertificateStatus update); +} diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java new file mode 100644 index 0000000000..d0612526a4 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ + +package org.epics.pva.common; + +import static org.epics.pva.PVASettings.logger; + +import java.util.BitSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; + +import org.epics.pva.client.ClientChannelState; +import org.epics.pva.client.PVAChannel; +import org.epics.pva.client.PVAClient; +import org.epics.pva.data.PVAStructure; +import org.epics.pva.data.nt.PVAEnum; + +/** Monitors the 'CERT:STATUS:...' PV for a certificate + * + *

A certificate might be valid based on its expiration date, + * but PVACMS can list a 'CERT:STATUS:...' PV in the certificate + * that tools which use the certificate are expected to monitor. + * + *

Without a 'VALID' state confirmed by the 'CERT:STATUS:...' PV, + * the certificate should not be used for authentication. + * + *

A server, for example, should consider the client 'anonymous' + * until the CERT:STATUS PV declares the certificate 'VALID', + * at which time the certificate will authenticate the principal user + * listed in the cert. + * + *

On a 'REVOKED' update, the server should again ignore the authentication + * info from the certificate and consider the peer anonymous. + * + *

Several client tools or IOCs might use the same certificate. + * This singleton performs the check only once per certificate + * and updates several listeners when the certificate validity changes. + * + * @author Kay Kasemir + */ +public class CertificateStatusMonitor +{ + /** Singleton instance */ + private static CertificateStatusMonitor instance = null; + + /** Map from CERT:STATUS:... PV name to CertificateStatus of that PV */ + private final ConcurrentHashMap certificate_states = new ConcurrentHashMap<>(); + + /** PVA Client used for all CERT:STATUS:... PVs */ + private PVAClient client = null; + + /** Certificate status: Valid or not? */ + public class CertificateStatus + { + private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); + private final String peer_name; + private final PVAChannel pv; + private String status = null; + + CertificateStatus(final String peer_name, final String status_pv_name) + { + this.peer_name = peer_name; + pv = client.getChannel(status_pv_name, this::handleConnection); + } + + /** @return CERT:STATUS:... PV name */ + public String getPVName() + { + return pv.getName(); + } + + /** @param listener Listener to add (with initial update) */ + void addListener(final CertificateStatusListener listener) + { + listeners.add(listener); + // Send initial update + logger.log(Level.FINER, "Initial " + getPVName() + " update"); + listener.handleCertificateStatusUpdate(this); + } + + /** @param listener Listener to remove + * @return Was that the last listener, can CertificateStatus be removed? + */ + boolean removeListener(final CertificateStatusListener listener) + { + if (! listeners.remove(listener)) + throw new IllegalStateException("Unknown CertificateStatusListener"); + return listeners.isEmpty(); + } + + /** @return Is the certificate currently valid? */ + public boolean isValid() + { + return "VALID".equals(status); + } + + private void handleConnection(final PVAChannel channel, final ClientChannelState state) + { + if (state == ClientChannelState.CONNECTED) + try + { + channel.subscribe("", this::handleMonitor); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot subscribe to " + pv, ex); + } + } + + private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, final PVAStructure data) + { + // TODO also check string ocsp_certified_until Mon Sep 22 19:37:25 2025 UTC? + // TODO Can those be time_t secondsPastEpoch? + // Decode overall status enum, VALID or not? + final PVAEnum value = PVAEnum.fromStructure(data.get("value")); + if (value != null) + { + status = value.enumString(); + logger.log(Level.FINER, () -> this.toString()); + + // Notify listeners + for (var listener : listeners) + listener.handleCertificateStatusUpdate(this); + } + else + logger.log(Level.WARNING, pv + " failed to send status, got " + data); + } + + /** Close the CERT:STATUS:... PV check */ + void close() + { + if (! listeners.isEmpty()) + throw new IllegalStateException(getPVName() + " is still in use"); + pv.close(); + } + + @Override + public String toString() + { + return pv.getName() + " for '" + peer_name + "' is " + status; + } + } + + /** Constructor of the singleton instance */ + private CertificateStatusMonitor() + { + try + { + client = new PVAClient(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot create PVAClient for CERT:STATUS:... monitor", ex); + } + } + + // Synchronization: + // + // - Creating/getting the singleton + // - checkCertStatus: Creates or gets CertificateStatus for PV name, adds listener + // - remove: Removes listener, closes CertificateStatus on removal of last listener + // + // Late CERT:STATUS.. monitor will call all listeners using a safe CopyOnWriteArray list + + /** @return Singleton instance */ + public static synchronized CertificateStatusMonitor instance() + { + if (instance == null) + instance = new CertificateStatusMonitor(); + return instance; + } + + /** @param status_pv_name CERT:STATUS:... PV name + * @param peer_name Name of the peer (principal of the certificate) + * @param listener Listener to invoke for certificate status updates + * @return {@link CertificateStatus} to which we're subscribed, need to unsubscribe when no longer needed + */ + public synchronized CertificateStatus checkCertStatus(final String status_pv_name, final String peer_name, final CertificateStatusListener listener) + { + if (!status_pv_name.startsWith("CERT:STATUS:")) + throw new IllegalArgumentException("Need CERT:STATUS:... PV"); + + logger.log(Level.FINER, () -> "Checking " + status_pv_name + " for '" + peer_name + "'"); + + CertificateStatus cert_stat = certificate_states.computeIfAbsent(status_pv_name, + stat_pv_name -> new CertificateStatus(peer_name, status_pv_name)); + cert_stat.addListener(listener); + + return cert_stat; + } + + public synchronized void remove(final CertificateStatus certificate_status, final CertificateStatusListener listener) + { + if (certificate_status.removeListener(listener)) + { + logger.log(Level.FINER, () -> "Stopping check of " + certificate_status.getPVName()); + certificate_status.close(); + if (! certificate_states.remove(certificate_status.getPVName(), certificate_status)) + throw new IllegalStateException("Unknown certificate status " + certificate_status); + } + } +} diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java b/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java index bb93b30477..0355eeca13 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2023 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,18 +7,17 @@ ******************************************************************************/ package org.epics.pva.common; -/** PVA Authentication/Authorization related constants +/** PVA Authentication options * @author Kay Kasemir */ -@SuppressWarnings("nls") -public class PVAAuth +public enum PVAAuth { /** Anonymous authentication */ - public static String ANONYMOUS = "anonymous"; + anonymous, /** CA authentication based on user name and host */ - public static String CA = "ca"; + ca, - /**Authentication based on 'Common Name' in certificate */ - public static String X509 = "x509"; + /** Authentication based on 'Common Name' in certificate */ + x509; } diff --git a/core/pva/src/main/java/org/epics/pva/server/ClientAuthentication.java b/core/pva/src/main/java/org/epics/pva/server/ClientAuthentication.java new file mode 100644 index 0000000000..6151a3a3a7 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/server/ClientAuthentication.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.server; + +import java.nio.ByteBuffer; + +import org.epics.pva.common.PVAAuth; +import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; +import org.epics.pva.data.PVAData; +import org.epics.pva.data.PVAString; +import org.epics.pva.data.PVAStructure; +import org.epics.pva.data.PVATypeRegistry; + +/** Determine authentication of a client connected to this server + * @author Kay Kasemir + */ +public class ClientAuthentication +{ + public final static ClientAuthentication Anonymous = new ClientAuthentication(PVAAuth.anonymous, "nobody", "nohost"); + + private final PVAAuth type; + private final String user, host; + + ClientAuthentication(final PVAAuth type, final String user, final String host) + { + this.type = type; + this.user = user; + this.host = host; + } + + /** @return Type of authentication */ + public PVAAuth getType() + { + return type; + } + + /** @return User name */ + public String getUser() + { + return user; + } + + /** @return Client's host */ + public String getHost() + { // TODO Use numeric IP address? Host name? InetAddress? + return host; + } + + @Override + public String toString() + { + return type + "(" + user + "@" + host + ")"; + } + + /** Decode authentication + * @param tcp TCP Handler + * @param buffer Buffer, positioned on String auth, followed by optional detail + * @param tls_info {@link TLSHandshakeInfo}, may be null + * @return {@link ClientAuthentication} + * @throws Exception on error + */ + public static ClientAuthentication decode(final ServerTCPHandler tcp, final ByteBuffer buffer, TLSHandshakeInfo tls_info) throws Exception + { + final String auth = PVAString.decodeString(buffer); + + if (buffer.remaining() < 1) + throw new Exception("Missing authentication detail for '" + auth + "'"); + + final PVATypeRegistry types = tcp.getClientTypes(); + final PVAData type = types.decodeType("", buffer); + if (type instanceof PVAStructure info) + { + info.decode(types, buffer); + + // CA authentication gets details from info structure + if (PVAAuth.ca.name().equals(auth)) + { + PVAString element = info.get("user"); + if (element == null) + throw new Exception("Missing 'ca' authentication 'user', got " + info); + final String user = element.get(); + + element = info.get("host"); + if (element == null) + throw new Exception("Missing 'ca' authentication 'host', got " + info); + final String host = element.get(); + return new ClientAuthentication(PVAAuth.ca, user, host); + } + else // For other authentication methods, there should be no additional info structure + if (info != null) + throw new Exception("Expected no authentication detail for '" + auth + "' but got " + info); + } + + if (PVAAuth.x509.name().equals(auth)) + return new ClientAuthentication(PVAAuth.x509, tls_info.name, tls_info.hostname); + + return new ClientAuthentication(PVAAuth.anonymous, "nobody", tcp.getRemoteAddress().getHostString()); + } +} diff --git a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java index f1ccdd9f52..768200e30c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java @@ -57,7 +57,7 @@ private void sendChannelCreated(final ServerTCPHandler tcp, final ServerPV pv, i { // Send initial access rights with (before) the channel confirmation, // so client knows permissions when channel is confirmed - final boolean writable = pv.isWritable(); + final boolean writable = pv.isWritable(tcp.getClientAuthentication()); logger.log(Level.FINE, () -> "Send ACL " + pv + " [CID " + cid + "]" + (writable ? " writable" : " read-only")); AccessRightsChange.encode(buffer, cid, writable); diff --git a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java index 57a1f890ac..fec14cfea8 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java @@ -34,7 +34,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PVAServer implements AutoCloseable { // TODO Implement beacons? @@ -56,7 +55,7 @@ public class PVAServer implements AutoCloseable /** TCP connection listener, creates {@link ServerTCPHandler} for each connecting client */ private final ServerTCPListener tcp; - /** Optional searche handler 'hook' */ + /** Optional search handler 'hook' */ private final SearchHandler custom_search_handler; /** Handlers for the TCP connections clients established to this server */ @@ -167,7 +166,7 @@ ServerPV getPV(final int sid) * Network address and authentication info */ public static record ClientInfo(InetSocketAddress address, - ServerAuth authentication) + ClientAuthentication authentication) { } @@ -178,7 +177,7 @@ public Collection getClientInfos() { return tcp_handlers.stream() .map(tcp -> new ClientInfo(tcp.getRemoteAddress(), - tcp.getAuth())) + tcp.getClientAuthentication())) .toList(); } @@ -257,6 +256,17 @@ void register(final ServerTCPHandler tcp_connection) tcp_handlers.add(tcp_connection); } + /** Called by {@link ServerTCPHandler} when authentication changes + * @param tcp_connection TCP connection that has updated authentication + * @param client_auth Client authentication + */ + void updatePermissions(final ServerTCPHandler tcp_connection, final ClientAuthentication client_auth) + { + logger.log(Level.FINE, () -> tcp_connection + " authentication changed: " + client_auth); + for (ServerPV pv : pv_by_name.values()) + pv.updatePermissions(tcp_connection, client_auth); + } + /** @param tcp_connection {@link ServerTCPHandler} that experienced error or client closed it */ void shutdownConnection(final ServerTCPHandler tcp_connection) { diff --git a/core/pva/src/main/java/org/epics/pva/server/PutHandler.java b/core/pva/src/main/java/org/epics/pva/server/PutHandler.java index 1432a699cd..3e3b0ebbf7 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PutHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/PutHandler.java @@ -23,7 +23,6 @@ /** Handle client's PUT command * @author Kay Kasemir */ -@SuppressWarnings("nls") class PutHandler implements CommandHandler { @Override @@ -80,7 +79,7 @@ else if (subcmd == 0 || subcmd == PVAHeader.CMD_SUB_DESTROY) final BitSet written = PVABitSet.decodeBitSet(buffer); // Check write access in general and for this client - if (! (pv.isWritable() && tcp.getAuth().hasWriteAccess(pv.getName()))) + if (! (pv.isWritable(tcp.getClientAuthentication()))) { GetHandler.sendError(tcp, PVAHeader.CMD_PUT, req, subcmd, "No write access to " + pv.getName()); return; diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java b/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java deleted file mode 100644 index 3a42265f7c..0000000000 --- a/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2019-2025 Oak Ridge National Laboratory. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - ******************************************************************************/ -package org.epics.pva.server; - -import java.nio.ByteBuffer; - -import org.epics.pva.common.PVAAuth; -import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; -import org.epics.pva.data.PVAData; -import org.epics.pva.data.PVAString; -import org.epics.pva.data.PVAStructure; -import org.epics.pva.data.PVATypeRegistry; - -/** Determine authorization of a client connected to this server - * @author Kay Kasemir - */ -@SuppressWarnings("nls") -public abstract class ServerAuth -{ - /** @param channel Channel for which to check write access - * @return Does client have write access? - */ - abstract public boolean hasWriteAccess(final String channel); - - // Must implement toString to describe auth - @Override - public abstract String toString(); - - /** Decode authentication and then determine authorizations - * @param tcp TCP Handler - * @param buffer Buffer, positioned on String auth, optional detail - * @param tls_info {@link TLSHandshakeInfo}, may be null - * @return ClientAuthorization - * @throws Exception on error - */ - public static ServerAuth decode(final ServerTCPHandler tcp, final ByteBuffer buffer, TLSHandshakeInfo tls_info) throws Exception - { - final String auth = PVAString.decodeString(buffer); - - if (buffer.remaining() < 1) - throw new Exception("Missing authentication detail for '" + auth + "'"); - - final PVATypeRegistry types = tcp.getClientTypes(); - final PVAData type = types.decodeType("", buffer); - PVAStructure info = null; - if (type instanceof PVAStructure) - { - info = (PVAStructure) type; - info.decode(types, buffer); - } - - if (PVAAuth.CA.equals(auth)) - return new CAServerAuth(info); - - if (info != null) - throw new Exception("Expected no authentication detail for '" + auth + "' but got " + info); - - if (PVAAuth.X509.equals(auth)) - return new X509ServerAuth(tls_info); - - return Anonymous; - } - - public static final ServerAuth Anonymous = new ServerAuth() - { - @Override - public boolean hasWriteAccess(final String channel) - { - return false; - } - - @Override - public String toString() - { - return PVAAuth.ANONYMOUS; - } - }; - - private static class CAServerAuth extends ServerAuth - { - private String user, host; - - public CAServerAuth(final PVAStructure info) throws Exception - { - PVAString element = info.get("user"); - if (element == null) - throw new Exception("Missing 'ca' authentication 'user', got " + info); - user = element.get(); - - element = info.get("host"); - if (element == null) - throw new Exception("Missing 'ca' authentication 'host', got " + info); - host = element.get(); - } - - @Override - public boolean hasWriteAccess(final String channel) - { - // TODO Implement access security based on `acf` type config file, checking channel for user and host - return true; - } - - @Override - public String toString() - { - return "ca(" + user + "@" + host + ")"; - } - } - - - private static class X509ServerAuth extends ServerAuth - { - private String user, host; - - public X509ServerAuth(final TLSHandshakeInfo tls_info) throws Exception - { - if (tls_info == null) - throw new Exception("x509 authentication requires principal name from TLS certificate"); - user = tls_info.name; - host = tls_info.hostname; - } - - @Override - public boolean hasWriteAccess(final String channel) - { - // TODO Implement access security based on `acf` type config file, checking channel for user and host - return true; - } - - @Override - public String toString() - { - return "x509(" + user + "@" + host + ")"; - } - } -} diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerAuthorization.java b/core/pva/src/main/java/org/epics/pva/server/ServerAuthorization.java new file mode 100644 index 0000000000..6ace6d2f45 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/server/ServerAuthorization.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.server; + +/** Determine authorization of a client connected to this server + * @author Kay Kasemir + */ +public class ServerAuthorization +{ + /** @param pv_name Channel for which to check write access + * @param client_auth Client authentication + * @return Does client have write access? + */ + public boolean hasWriteAccess(final String pv_name, final ClientAuthentication client_auth) + { + // TODO Implement authorization based on for example an ACF file + return true; + } +} diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerPV.java b/core/pva/src/main/java/org/epics/pva/server/ServerPV.java index 5329bc3c14..23e84e155c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerPV.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerPV.java @@ -17,6 +17,7 @@ import java.util.logging.Level; import org.epics.pva.common.AccessRightsChange; +import org.epics.pva.common.PVAAuth; import org.epics.pva.common.PVAHeader; import org.epics.pva.data.PVAString; import org.epics.pva.data.PVAStructure; @@ -28,7 +29,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ServerPV implements AutoCloseable { /** Value used when accessing an RPC PV as a data PV */ @@ -147,6 +147,26 @@ void addClient(final ServerTCPHandler tcp, final int cid) logger.log(Level.WARNING, "Client " + tcp + " requested " + this + " as CID " + cid + " but also " + other); } + /** Called by CertificateStatusMonitor via ServerTCPHandler and PVAServer + * @param tcp {@link ServerTCPHandler} with new client authentication info + * @param client_auth {@link ClientAuthentication} + */ + void updatePermissions(final ServerTCPHandler tcp, final ClientAuthentication client_auth) + { + // Is PV accessed via that TCP/TLS connection? + final Integer cid = cid_by_client.get(tcp); + if (cid != null) + { + // Does the client have write access? + final boolean writable = isWritable(client_auth); + tcp.submit((version, buffer) -> + { + logger.log(Level.FINE, () -> "Send ACL " + this + " [CID " + cid + "]" + (writable ? " writable" : " read-only")); + AccessRightsChange.encode(buffer, cid, writable); + }); + } + } + /** Un-register a client of this PV * @param tcp TCP connection to client * @param cid Client's ID for this PV (-1 to remove any) @@ -154,13 +174,17 @@ void addClient(final ServerTCPHandler tcp, final int cid) void removeClient(final ServerTCPHandler tcp, final int cid) { // Stop associating PV with that TCP connection - final Integer other = cid_by_client.remove(tcp); - if (cid == -1) - logger.log(Level.FINE, "Client " + tcp + " released " + this + " [CID was " + other + "]"); - else if (other != null && other.intValue() == cid) + final Integer original_cid = cid_by_client.remove(tcp); + // Did we never deal with this PV via that TCP connection? + if (cid == -1 && original_cid == null) + return; + else if (cid == -1) + logger.log(Level.FINE, "Client " + tcp + " released " + this + " [CID was " + original_cid + "]"); + else if (original_cid != null && original_cid.intValue() == cid) logger.log(Level.FINE, "Client " + tcp + " released " + this + " [CID " + cid + "]"); else - logger.log(Level.WARNING, "Client " + tcp + " released " + this + " as CID " + cid + " instead of " + other); + // Our memory of the cid differs from what the client now uses to release the PV?!? + logger.log(Level.WARNING, "Client " + tcp + " released " + this + " as CID " + cid + " instead of " + original_cid); // Delete all subscriptions to this PV from that TCP connection // A perfect client would separately clear the subscription, @@ -232,10 +256,15 @@ PVAStructure getData() } } - /** @return Is the PV writable? */ - public boolean isWritable() + /** @param client_auth Client authentication + * @return Is the PV writable by that client? + */ + public boolean isWritable(final ClientAuthentication client_auth) { - return writable.get(); + // For now, as long as PV supports write access, + // any authenticated user (CA or X509) can write + // TODO Check user in ServerAuthorization + return writable.get() && client_auth.getType() != PVAAuth.anonymous; } /** Update write access @@ -246,11 +275,16 @@ public boolean isWritable() */ public void setWritable(final boolean writable) { + // Change in overall write support of this PV? if (write_handler != READONLY_WRITE_HANDLER && this.writable.compareAndSet(!writable, writable)) { + // For each TCP/TLS connection, get authenticated user and compute access rights logger.log(Level.FINE, () -> "Update ACL " + this + (writable ? " to writable" : " to read-only")); cid_by_client.forEach((tcp, cid) -> - tcp.submit((version, buffer) -> AccessRightsChange.encode(buffer, cid, writable))); + { + boolean effective = isWritable(tcp.getClientAuthentication()); + tcp.submit((version, buffer) -> AccessRightsChange.encode(buffer, cid, effective)); + }); } } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java index e56070105c..042fe2ff2c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java @@ -15,6 +15,9 @@ import java.util.Objects; import java.util.logging.Level; +import org.epics.pva.common.CertificateStatusListener; +import org.epics.pva.common.CertificateStatusMonitor; +import org.epics.pva.common.CertificateStatusMonitor.CertificateStatus; import org.epics.pva.common.CommandHandlers; import org.epics.pva.common.PVAAuth; import org.epics.pva.common.PVAHeader; @@ -29,7 +32,6 @@ /** Handler for one TCP-connected client * @author Kay Kasemir */ -@SuppressWarnings("nls") class ServerTCPHandler extends TCPHandler { /** Handlers for various commands, re-used whenever a command is received */ @@ -56,9 +58,14 @@ class ServerTCPHandler extends TCPHandler /** Types declared by client at other end of this TCP connection */ private final PVATypeRegistry client_types = new PVATypeRegistry(); - /** Auth info, e.g. client user info and his/her permissions */ - private volatile ServerAuth auth = ServerAuth.Anonymous; + /** Client authentication */ + private volatile ClientAuthentication client_auth = ClientAuthentication.Anonymous; + /** {@link CertificateStatus} that we monitor for the TLS connection */ + private CertificateStatus certificate_status = null; + + /** Handler for updates from {@link CertificateStatusMonitor} */ + private final CertificateStatusListener certificate_status_listener; public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHandshakeInfo tls_info) throws Exception { @@ -69,6 +76,27 @@ public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHa this.tls_info = tls_info; server.register(this); + + certificate_status_listener = update-> + { + final ClientAuthentication auth = getClientAuthentication(); + logger.log(Level.FINER, "Certificate update for " + this + ": " + auth); + + // 1) Initial client_auth is Anonymous + // When TLS connection starts, + // 2a) CertificateStatusMonitor looks for CERT:STATUS:.., initial update has Anonymous from 1) + // 2b) ValidationHandler will setClientAuthentication(x509 info from TLS) + // If somebody called getClientAuthentication(), they'd get Anon/invalid because no "Valid" update, yet + // 3) "Valid" update from CertificateStatusMonitor tends to happen just after that + // --> Update all ServerPVs to send AccessRightsChange, in case there are already Server PVs + server.updatePermissions(this, auth); + + // Channel created? CreateChannelHandler.sendChannelCreated sends initial AccessRightsChange + // ServerPV.setWritable will send updated AccessRightsChange + }; + if (tls_info != null && !tls_info.status_pv_name.isEmpty()) + certificate_status = CertificateStatusMonitor.instance().checkCertStatus(tls_info.status_pv_name, tls_info.name, certificate_status_listener); + startReceiver(); startSender(); @@ -106,10 +134,10 @@ public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHa // string[] authNZ; listing most secure at end PVASize.encodeSize(support_x509 ? 3 : 2, buffer); - PVAString.encodeString(PVAAuth.ANONYMOUS, buffer); - PVAString.encodeString(PVAAuth.CA, buffer); + PVAString.encodeString(PVAAuth.anonymous.name(), buffer); + PVAString.encodeString(PVAAuth.ca.name(), buffer); if (support_x509) - PVAString.encodeString(PVAAuth.X509, buffer); + PVAString.encodeString(PVAAuth.x509.name(), buffer); buffer.putInt(size_offset, buffer.position() - payload_start); }); @@ -143,20 +171,31 @@ PVATypeRegistry getClientTypes() return client_types; } - void setAuth(final ServerAuth auth) + /** @param client_auth Client authentication */ + void setClientAuthentication(final ClientAuthentication client_auth) { - this.auth = auth; + this.client_auth = client_auth; } - // XXX At this time, nothing uses the auth info - ServerAuth getAuth() + /** @return How did the client authenticate? */ + ClientAuthentication getClientAuthentication() { - return auth; + // Do we have a certificate from the TLS connection, but CERT:STATUS:.. doesn't declare it valid? + // --> Fall back to anonymous + if (certificate_status != null && !certificate_status.isValid()) + return new ClientAuthentication(PVAAuth.anonymous, "invalid/" + client_auth.getUser(), client_auth.getHost()); + + return client_auth; } @Override protected void onReceiverExited(final boolean running) { + if (certificate_status != null) + { + CertificateStatusMonitor.instance().remove(certificate_status, certificate_status_listener); + certificate_status = null; + } if (running) server.shutdownConnection(this); } diff --git a/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java b/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java index cacad6bb97..3ffb2e011f 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2023 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,7 +19,6 @@ /** Handle response clients's VALIDATION reply * @author Kay Kasemir */ -@SuppressWarnings("nls") class ValidationHandler implements CommandHandler { @Override @@ -39,9 +38,9 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t final int client_registry_size = Short.toUnsignedInt(buffer.getShort()); final short quos = buffer.getShort(); - final ServerAuth auth = ServerAuth.decode(tcp, buffer, tcp.getTLSHandshakeInfo()); - logger.log(Level.FINE, "Connection validated, auth '" + auth + "'"); - tcp.setAuth(auth); + final ClientAuthentication auth = ClientAuthentication.decode(tcp, buffer, tcp.getTLSHandshakeInfo()); + logger.log(Level.FINE, "Connection validated, authentication '" + auth + "'"); + tcp.setClientAuthentication(auth); sendConnectionValidated(tcp); } From 1d52796bf80199d0b81d6758df5f1b69c7584128 Mon Sep 17 00:00:00 2001 From: kasemir Date: Mon, 3 Nov 2025 14:19:19 -0500 Subject: [PATCH 2/7] Review PVA log levels See PVASettings.logger for intended log level usage --- core/pva/serverdemo | 4 +- .../main/java/org/epics/pva/PVASettings.java | 15 ++++- .../org/epics/pva/client/ChannelSearch.java | 6 +- .../epics/pva/client/ClientTCPHandler.java | 3 +- .../epics/pva/client/ClientUDPHandler.java | 4 +- .../org/epics/pva/client/PVAClientMain.java | 13 ++-- .../pva/common/CertificateStatusMonitor.java | 5 +- .../java/org/epics/pva/common/TCPHandler.java | 13 ++-- .../java/org/epics/pva/common/UDPHandler.java | 9 ++- .../pva/server/CreateChannelHandler.java | 2 +- .../pva/server/DestroyChannelHandler.java | 5 +- .../java/org/epics/pva/server/PVAServer.java | 4 +- .../java/org/epics/pva/server/ServerDemo.java | 62 ++++++++++++++++++- .../epics/pva/server/ServerTCPHandler.java | 5 +- .../epics/pva/server/ServerUDPHandler.java | 14 ++--- 15 files changed, 114 insertions(+), 50 deletions(-) diff --git a/core/pva/serverdemo b/core/pva/serverdemo index 15751ee7f3..f6ee0de6d5 100755 --- a/core/pva/serverdemo +++ b/core/pva/serverdemo @@ -4,8 +4,8 @@ JAR=`echo target/core-pva*.jar` if [ -r "$JAR" ] then # Echo use jar file - java -cp $JAR org.epics.pva.server.ServerDemo + java -cp $JAR org.epics.pva.server.ServerDemo "$@" else # Use build output - java -cp target/classes org.epics.pva.server.ServerDemo + java -cp target/classes org.epics.pva.server.ServerDemo "$@" fi diff --git a/core/pva/src/main/java/org/epics/pva/PVASettings.java b/core/pva/src/main/java/org/epics/pva/PVASettings.java index 9635b012c7..0c36568ede 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2023 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,10 +18,19 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PVASettings { - /** Common logger */ + /** Common logger + * + * Usage of levels: + *

+ */ public static final Logger logger = Logger.getLogger(PVASettings.class.getPackage().getName()); /** Address list. diff --git a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java index 7d434e69ce..d4e9a49292 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java +++ b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java @@ -29,7 +29,6 @@ import org.epics.pva.common.AddressInfo; import org.epics.pva.common.RequestEncoder; import org.epics.pva.common.SearchRequest; -import org.epics.pva.data.Hexdump; import org.epics.pva.data.PVAString; /** Handler for search requests @@ -65,7 +64,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") class ChannelSearch { /** Basic search period is one second */ @@ -518,7 +516,7 @@ private void sendSearch(final int seq, final Collection c try { logger.log(Level.FINER, () -> "Sending search to UDP " + addr + " (unicast), " + - "response addr " + response + "\n" + Hexdump.toHexdump(send_buffer)); + "response addr " + response); udp.send(send_buffer, addr); } catch (Exception ex) @@ -536,7 +534,7 @@ private void sendSearch(final int seq, final Collection c try { logger.log(Level.FINER, () -> "Sending search to UDP " + addr + " (broadcast/multicast), " + - "response addr " + response + "\n" + Hexdump.toHexdump(send_buffer)); + "response addr " + response); udp.send(send_buffer, addr); } catch (Exception ex) diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java index 10e1eb1c11..5e4f9b4ab6 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java @@ -39,7 +39,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") class ClientTCPHandler extends TCPHandler { private static final CommandHandlers handlers = @@ -112,7 +111,7 @@ class ClientTCPHandler extends TCPHandler public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid, final boolean tls) throws Exception { super(true); - logger.log(Level.FINE, () -> "TCPHandler " + (tls ? "(TLS) " : "") + guid + " for " + address + " created ============================"); + logger.log(Level.FINER, () -> "TCPHandler " + (tls ? "(TLS) " : "") + guid + " for " + address + " created ============================"); this.server_address = address; this.tls = tls; this.client = client; diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java index 66bf3cf3d0..8205f4e61d 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java @@ -37,7 +37,6 @@ /** Sends and receives search replies, monitors beacons * @author Kay Kasemir */ -@SuppressWarnings("nls") class ClientUDPHandler extends UDPHandler { @FunctionalInterface @@ -122,7 +121,7 @@ public ClientUDPHandler(final BeaconHandler beacon_handler, ipV6Msg = ""; } String logMsg = String.format("Awaiting search replies on UDP %s%s and beacons on %s", udp_localaddr4, ipV6Msg, Network.getLocalAddress(udp_beacon)); - logger.log(Level.FINE, logMsg); + logger.log(Level.CONFIG, logMsg); } /** @param target Address to which message will be sent @@ -135,6 +134,7 @@ InetSocketAddress getResponseAddress(final AddressInfo target) public void send(final ByteBuffer buffer, final AddressInfo info) throws Exception { + logger.log(Level.FINEST, () -> "Sending UDP to " + info.getAddress() + "\n" + Hexdump.toHexdump(buffer)); // synchronized (udp_search)? // Not necessary based on Javadoc for send(), // but in case we set the multicast IF & TTL diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java index 0877bc3bd8..b4216f84a5 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java @@ -33,7 +33,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PVAClientMain { private static double seconds = 5.0; @@ -100,8 +99,8 @@ private static void info(final List names) throws Exception final PVAChannel pv = iter.next(); if (pv.getState() == ClientChannelState.CONNECTED) { - PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.FINE, "Server X509 Name: " + pv.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.FINE, "Client X509 Name: " + pv.getTCP().getClientX509Name()); final PVAData data = pv.info(request).get(timeout_ms, TimeUnit.MILLISECONDS); System.out.println(pv.getName() + " = " + data.formatType()); @@ -145,8 +144,8 @@ private static void get(final List names) throws Exception final PVAChannel pv = iter.next(); if (pv.getState() == ClientChannelState.CONNECTED) { - PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.FINE, "Server X509 Name: " + pv.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.FINE, "Client X509 Name: " + pv.getTCP().getClientX509Name()); final PVAData data = pv.read(request).get(timeout_ms, TimeUnit.MILLISECONDS); System.out.println(pv.getName() + " = " + data); @@ -191,8 +190,8 @@ private static void monitor(final List names) throws Exception { try { - PVASettings.logger.log(Level.INFO, "Server X509 Name: " + ch.getTCP().getServerX509Name()); - PVASettings.logger.log(Level.INFO, "Client X509 Name: " + ch.getTCP().getClientX509Name()); + PVASettings.logger.log(Level.FINE, "Server X509 Name: " + ch.getTCP().getServerX509Name()); + PVASettings.logger.log(Level.FINE, "Client X509 Name: " + ch.getTCP().getClientX509Name()); ch.subscribe(request, listener); } catch (Exception ex) diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java index d0612526a4..8a671d01aa 100644 --- a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java @@ -115,9 +115,10 @@ private void handleConnection(final PVAChannel channel, final ClientChannelState private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, final PVAStructure data) { - // TODO also check string ocsp_certified_until Mon Sep 22 19:37:25 2025 UTC? - // TODO Can those be time_t secondsPastEpoch? + logger.log(Level.FINE, () -> "Received " + channel.getName() + " = " + data); + // Decode overall status enum, VALID or not? + // TODO Check ocsp_response final PVAEnum value = PVAEnum.fromStructure(data.get("value")); if (value != null) { diff --git a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java index 1ea1a2aaa9..55a13fce27 100644 --- a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java @@ -39,7 +39,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") abstract public class TCPHandler { /** Protocol version used by the PVA server @@ -233,7 +232,7 @@ private Void sender() */ protected void send(final ByteBuffer buffer) throws Exception { - logger.log(Level.FINER, () -> Thread.currentThread().getName() + " sends:\n" + Hexdump.toHexdump(buffer)); + logger.log(Level.FINEST, () -> Thread.currentThread().getName() + " sends:\n" + Hexdump.toHexdump(buffer)); // Original AbstractCodec.send() mentions // Microsoft KB article KB823764: @@ -276,7 +275,7 @@ private Void receiver() // Listen on the connection Thread.currentThread().setName("TCP receiver " + socket.getLocalSocketAddress()); logger.log(Level.FINER, () -> Thread.currentThread().getName() + " started for " + socket.getRemoteSocketAddress()); - logger.log(Level.FINER, "Native byte order " + receive_buffer.order()); + logger.log(Level.FINEST, "Native byte order " + receive_buffer.order()); receive_buffer.clear(); final InputStream in = socket.getInputStream(); while (true) @@ -294,7 +293,7 @@ private Void receiver() return null; } if (read > 0) - logger.log(Level.FINER, () -> Thread.currentThread().getName() + ": " + read + " bytes"); + logger.log(Level.FINEST, () -> Thread.currentThread().getName() + ": " + read + " bytes"); receive_buffer.position(receive_buffer.position() + read); // and once we get the header, it will tell // us how large the message actually is @@ -302,7 +301,7 @@ private Void receiver() } // .. then decode receive_buffer.flip(); - logger.log(Level.FINER, () -> Thread.currentThread().getName() + " received:\n" + Hexdump.toHexdump(receive_buffer)); + logger.log(Level.FINEST, () -> Thread.currentThread().getName() + " received:\n" + Hexdump.toHexdump(receive_buffer)); // While buffer may contain more data, // limit it to the end of this message to prevent @@ -560,7 +559,7 @@ protected void handleApplicationMessage(final byte command, final ByteBuffer buf */ public void close(final boolean wait) { - logger.log(Level.FINE, "Closing " + this); + logger.log(Level.FINER, "Closing " + this); // Wait until all requests are sent out submit(END_REQUEST); @@ -585,7 +584,7 @@ public void close(final boolean wait) { logger.log(Level.WARNING, "Cannot stop receive thread", ex); } - logger.log(Level.FINE, () -> this + " closed ============================"); + logger.log(Level.FINER, () -> this + " closed ============================"); } @Override diff --git a/core/pva/src/main/java/org/epics/pva/common/UDPHandler.java b/core/pva/src/main/java/org/epics/pva/common/UDPHandler.java index 155176be13..ec15c58beb 100644 --- a/core/pva/src/main/java/org/epics/pva/common/UDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/common/UDPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -20,7 +20,6 @@ /** Base for handling UDP traffic * @author Kay Kasemir */ -@SuppressWarnings("nls") abstract public class UDPHandler { /** Keep running? */ @@ -32,7 +31,7 @@ abstract public class UDPHandler */ protected void listen(final DatagramChannel udp, final ByteBuffer buffer) { - logger.log(Level.FINE, "Starting " + Thread.currentThread().getName()); + logger.log(Level.FINER, "Starting " + Thread.currentThread().getName()); final String local = Network.getLocalAddress(udp); while (running) { @@ -45,7 +44,7 @@ protected void listen(final DatagramChannel udp, final ByteBuffer buffer) // XXX Check against list of ignored addresses? - logger.log(Level.FINER, () -> "Received UDP from " + from + " on " + local + "\n" + Hexdump.toHexdump(buffer)); + logger.log(Level.FINEST, () -> "Received UDP from " + from + " on " + local + "\n" + Hexdump.toHexdump(buffer)); handleMessages(from, buffer); } catch (Exception ex) @@ -55,7 +54,7 @@ protected void listen(final DatagramChannel udp, final ByteBuffer buffer) // else: Ignore, closing } } - logger.log(Level.FINE, "Exiting " + Thread.currentThread().getName()); + logger.log(Level.FINER, "Exiting " + Thread.currentThread().getName()); } /** Handle one or more reply messages diff --git a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java index 768200e30c..0abfa51f5c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/CreateChannelHandler.java @@ -44,7 +44,7 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t logger.log(Level.WARNING, () -> "Channel create request for unknown PV '" + name + "'"); else { - logger.log(Level.FINE, () -> "Channel create request '" + name + "', cid " + cid); + logger.log(Level.FINE, () -> "Channel create request '" + name + "' [CID " + cid + "]"); pv.addClient(tcp, cid); sendChannelCreated(tcp, pv, cid); } diff --git a/core/pva/src/main/java/org/epics/pva/server/DestroyChannelHandler.java b/core/pva/src/main/java/org/epics/pva/server/DestroyChannelHandler.java index 444f3d3798..4347691e92 100644 --- a/core/pva/src/main/java/org/epics/pva/server/DestroyChannelHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/DestroyChannelHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * Copyright (c) 2019-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,7 +18,6 @@ /** Handle client's DESTROY_CHANNEL command * @author Kay Kasemir */ -@SuppressWarnings("nls") class DestroyChannelHandler implements CommandHandler { @Override @@ -50,7 +49,7 @@ private void sendChannelDetroyed(final ServerTCPHandler tcp, final int cid, fina { tcp.submit((version, buffer) -> { - logger.log(Level.FINE, () -> "Sending destroy channel confirmation for SID " + sid + ", cid " + cid); + logger.log(Level.FINE, () -> "Sending destroy channel confirmation for SID " + sid + ", CID " + cid); PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_DESTROY_CHANNEL, 4+4); buffer.putInt(sid); buffer.putInt(cid); diff --git a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java index fec14cfea8..e545b06389 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java @@ -240,12 +240,12 @@ else if (tcp_connection != null) final ServerPV pv = getPV(name); if (pv != null) { // Reply with TCP connection info - logger.log(Level.FINE, () -> "Received Search for known PV " + pv); + logger.log(Level.FINE, () -> "Received Search for known PV " + pv + " [CID " + cid + "]"); send_search_reply.accept(null); return true; } else - logger.log(Level.FINE, () -> "Ignoring search for unknown PV '" + name + "'"); + logger.log(Level.FINER, () -> "Ignoring search for unknown PV '" + name + "'"); } return false; } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java index 2f4eaa3a7d..838e86fe8f 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogManager; +import java.util.logging.Logger; import org.epics.pva.PVASettings; import org.epics.pva.data.PVADouble; @@ -28,11 +29,68 @@ */ public class ServerDemo { + private static void help() + { + System.out.println("USAGE: ServerDemo [options]"); + System.out.println(); + System.out.println("Options:"); + System.out.println(" -h Help"); + System.out.println(" -v Verbosity, level 0-5"); + } + + private static void setLogLevel(final Level level) + { + // Cannot use PVASettings.logger here because that would + // construct it and log CONFIG messages before we might be + // able to disable them + Logger.getLogger("org.epics.pva").setLevel(level); + Logger.getLogger("jdk.event.security").setLevel(level); + } + public static void main(String[] args) throws Exception { - // Log everything LogManager.getLogManager().readConfiguration(PVASettings.class.getResourceAsStream("/pva_logging.properties")); - PVASettings.logger.setLevel(Level.ALL); + + for (int i=0; i "TCPHandler " + (tls_info != null ? "(TLS) " : "") + "for " + client.getRemoteSocketAddress() + " created ============================"); + // Server received the client socket from `accept` this.socket = Objects.requireNonNull(client); this.server = Objects.requireNonNull(server); @@ -80,7 +83,7 @@ public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHa certificate_status_listener = update-> { final ClientAuthentication auth = getClientAuthentication(); - logger.log(Level.FINER, "Certificate update for " + this + ": " + auth); + logger.log(Level.FINER, () -> "Certificate update for " + this + ": " + auth); // 1) Initial client_auth is Anonymous // When TLS connection starts, diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java index 34d5729a46..47cd66cd78 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java @@ -34,7 +34,6 @@ /** Listen to search requests, send beacons * @author Kay Kasemir */ -@SuppressWarnings("nls") class ServerUDPHandler extends UDPHandler { private final PVAServer server; @@ -75,7 +74,7 @@ public ServerUDPHandler(final PVAServer server) throws Exception if (udp4 != null) throw new Exception("EPICS_PVAS_INTF_ADDR_LIST has more than one IPv4 address"); udp4 = Network.createUDP(StandardProtocolFamily.INET, info.getAddress().getAddress(), PVASettings.EPICS_PVAS_BROADCAST_PORT); - logger.log(Level.FINE, "Awaiting searches and sending beacons on UDP " + info); + logger.log(Level.CONFIG, "Awaiting searches and sending beacons on UDP " + info); } if (info.isIPv6()) @@ -85,7 +84,7 @@ public ServerUDPHandler(final PVAServer server) throws Exception if (udp6 != null) throw new Exception("EPICS_PVAS_INTF_ADDR_LIST has more than one IPv6 address"); udp6 = Network.createUDP(StandardProtocolFamily.INET6, info.getAddress().getAddress(), PVASettings.EPICS_PVAS_BROADCAST_PORT); - logger.log(Level.FINE, "Awaiting searches and sending beacons on UDP " + info); + logger.log(Level.CONFIG, "Awaiting searches and sending beacons on UDP " + info); } } else @@ -101,7 +100,7 @@ public ServerUDPHandler(final PVAServer server) throws Exception udp4.setOption(StandardSocketOptions.IP_MULTICAST_IF, info.getInterface()); // Configure socket channel to receive from the multicast group udp4.join(info.getAddress().getAddress(), info.getInterface()); - logger.log(Level.FINE, "Listening to UDP multicast " + info); + logger.log(Level.CONFIG, "Listening to UDP multicast " + info); local_multicast = info; } if (info.isIPv6()) @@ -109,13 +108,13 @@ public ServerUDPHandler(final PVAServer server) throws Exception if (udp6 == null) throw new Exception("EPICS_PVAS_INTF_ADDR_LIST lacks IPv6 address, cannot add multicast"); udp6.join(info.getAddress().getAddress(), info.getInterface()); - logger.log(Level.FINE, "Listening to UDP multicast " + info); + logger.log(Level.CONFIG, "Listening to UDP multicast " + info); } } } if (local_multicast != null) - logger.log(Level.FINE, "IPv4 unicasts are re-transmitted via local multicast " + local_multicast); + logger.log(Level.CONFIG, "IPv4 unicasts are re-transmitted via local multicast " + local_multicast); if (udp4 != null) { @@ -255,7 +254,8 @@ public void sendSearchReply(final Guid guid, final int seq, final int cid, final send_buffer.clear(); SearchResponse.encode(guid, seq, cid, server_address.getAddress(), server_address.getPort(), tls, send_buffer); send_buffer.flip(); - logger.log(Level.FINER, () -> "Sending UDP search reply to " + client + "\n" + Hexdump.toHexdump(send_buffer)); + logger.log(Level.FINE, () -> "Sending UDP search reply for CID " + cid + " to " + client); + logger.log(Level.FINEST, () -> "Sending UDP to " + client + "\n" + Hexdump.toHexdump(send_buffer)); try { From ad4491f30c6586b4e2bd2856c00e3467cc1d9c6c Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 5 Nov 2025 10:34:35 -0500 Subject: [PATCH 3/7] AccessRightsChange decode logged a "server" messages as "client" --- .../main/java/org/epics/pva/common/AccessRightsChange.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java b/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java index 699956dd9b..a66a46c651 100644 --- a/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java +++ b/core/pva/src/main/java/org/epics/pva/common/AccessRightsChange.java @@ -76,11 +76,11 @@ public static AccessRightsChange decode(final InetSocketAddress from, { if (payload < PAYLOAD_SIZE) { - logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for access rights change"); + logger.log(Level.WARNING, "PVA server " + from + " sent only " + payload + " bytes for access rights change"); return null; } final AccessRightsChange acl = new AccessRightsChange(buffer.getInt(), buffer.get()); - logger.log(Level.FINER, () -> "PVA client " + from + " sent " + acl); + logger.log(Level.FINER, () -> "PVA server " + from + " sent " + acl); return acl; } From afee9d578e9bc4c6b07b46743e76ba84aaccd209 Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 5 Nov 2025 11:19:46 -0500 Subject: [PATCH 4/7] PVA server: Check OCSP response in CERT:STATUS:.. PV --- core/pva/.classpath | 1 + core/pva/pom.xml | 13 ++ .../pva/common/CertificateStatusMonitor.java | 160 +++++++++++++++--- .../org/epics/pva/common/SecureSockets.java | 70 ++++++-- .../epics/pva/server/ServerTCPHandler.java | 2 +- dependencies/phoebus-target/pom.xml | 12 ++ 6 files changed, 227 insertions(+), 31 deletions(-) diff --git a/core/pva/.classpath b/core/pva/.classpath index b50ff51447..a5a05d07ae 100644 --- a/core/pva/.classpath +++ b/core/pva/.classpath @@ -6,5 +6,6 @@ + diff --git a/core/pva/pom.xml b/core/pva/pom.xml index b49570e1e5..dcfa7e1896 100644 --- a/core/pva/pom.xml +++ b/core/pva/pom.xml @@ -20,6 +20,19 @@ 1.3 test + + + + org.bouncycastle + bcpkix-jdk18on + 1.82 + + + org.bouncycastle + bcprov-jdk18on + 1.82 + + diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java index 8a671d01aa..fa6e8662ad 100644 --- a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java @@ -10,14 +10,28 @@ import static org.epics.pva.PVASettings.logger; +import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.BitSet; +import java.util.Date; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.epics.pva.client.ClientChannelState; import org.epics.pva.client.PVAChannel; import org.epics.pva.client.PVAClient; +import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; +import org.epics.pva.data.Hexdump; +import org.epics.pva.data.PVAByteArray; import org.epics.pva.data.PVAStructure; import org.epics.pva.data.nt.PVAEnum; @@ -59,13 +73,15 @@ public class CertificateStatusMonitor public class CertificateStatus { private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); + private final X509Certificate certificate; private final String peer_name; private final PVAChannel pv; private String status = null; - CertificateStatus(final String peer_name, final String status_pv_name) + CertificateStatus(final X509Certificate certificate, final String status_pv_name) { - this.peer_name = peer_name; + this.certificate = certificate; + this.peer_name = certificate.getSubjectX500Principal().getName(); pv = client.getChannel(status_pv_name, this::handleConnection); } @@ -115,22 +131,123 @@ private void handleConnection(final PVAChannel channel, final ClientChannelState private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, final PVAStructure data) { - logger.log(Level.FINE, () -> "Received " + channel.getName() + " = " + data); - - // Decode overall status enum, VALID or not? - // TODO Check ocsp_response + // Check overall status enum: VALID or UNKNOWN, PENDING, REVOKED, ... final PVAEnum value = PVAEnum.fromStructure(data.get("value")); if (value != null) - { status = value.enumString(); - logger.log(Level.FINER, () -> this.toString()); + else + status = "UNKNOWN"; + logger.log(Level.FINE, () -> "Received " + channel.getName() + " = " + status); + logger.log(Level.FINER, () -> data.toString()); + + try + { + // Check OCSP Response bundled in the PVA structure + final PVAByteArray raw = data.get("ocsp_response"); + if (raw == null) + throw new Exception("Missing 'ocsp_response' in " + data); + + // Is it a successful OCSP response ... + final OCSPResp ocsp_response = new OCSPResp(raw.get()); + if (ocsp_response.getStatus() != OCSPResp.SUCCESSFUL) + throw new Exception("OCSP Response status " + ocsp_response.getStatus()); + // ...with "basic" info? + if (! (ocsp_response.getResponseObject() instanceof BasicOCSPResp)) + throw new Exception("Expected BasicOCSPResp, got " + ocsp_response.getResponseObject()); + final BasicOCSPResp basic = (BasicOCSPResp) ocsp_response.getResponseObject(); + logger.log(Level.FINER, () -> "OCSP responder " + basic.getResponderId().toASN1Primitive().getName()); + + // Validate against certificates in our key chain + boolean valid = false; + for (X509Certificate x509 : SecureSockets.keychain_x509_certificates.values()) + if (basic.isSignatureValid(new JcaContentVerifierProviderBuilder().build(x509))) + { + logger.log(Level.FINER, () -> "OCSP response verified by " + x509.getSubjectX500Principal()); + valid = true; + break; + } + if (! valid) + throw new Exception("Cannot validate OCSP response"); + + // AuthorityKeyIdentifier, public key of "EPICS Root Certificate Authority" + final JcaX509CertificateHolder bc_cert = new JcaX509CertificateHolder(certificate); + final byte[] authority_key_id = AuthorityKeyIdentifier.fromExtensions(bc_cert.getExtensions()).getKeyIdentifierOctets(); + if (authority_key_id == null) + throw new Exception("Cannot get AuthorityKeyIdentifier from " + certificate); + + // OCSP can include one or more responses. Find one that confirms the certificate + boolean ocsp_confirmation = false; + for (SingleResp response : basic.getResponses()) + { + // Is response for the certificate we want to check? + // Same authority? + final CertificateID id = response.getCertID(); + if (! Arrays.equals(authority_key_id, id.getIssuerKeyHash())) + { + logger.log(Level.FINER, () -> "OCSP authority\n" + Hexdump.toHexdump(id.getIssuerKeyHash()) + + "\ndiffers from\n" + Hexdump.toHexdump(authority_key_id)); + continue; + } + + // Same serial number? + if (! id.getSerialNumber().equals(certificate.getSerialNumber())) + { + logger.log(Level.FINER, () -> "OCSP Serial: 0x" + id.getSerialNumber().toString(16) + + " differs from expected 0x" + certificate.getSerialNumber().toString(16)); + continue; + } - // Notify listeners - for (var listener : listeners) - listener.handleCertificateStatusUpdate(this); + // Is applicable time range from <= now <= until? until may be null... + final Date now = new Date(), from = response.getThisUpdate(), until = response.getNextUpdate(); + if (from.after(now) || (until != null && now.after(until))) + { + logger.log(Level.FINER, () -> "Applicable time range from " + response.getThisUpdate() + + " to " + response.getNextUpdate() + " does not include now, " + now); + continue; + } + + // Seems to apply to the certificate we want to check. + + // What is the status? OCSP only indicates null for valid, RevokedStatus with revocation date, or UnknownStatus + // Use that to potentially update the more detailed + final org.bouncycastle.cert.ocsp.CertificateStatus response_status = response.getCertStatus(); + if (response_status == org.bouncycastle.cert.ocsp.CertificateStatus.GOOD) + { + logger.log(Level.FINER, "OCSP status is VALID"); + status = "VALID"; + ocsp_confirmation = true; + break; + } + else if (response_status instanceof RevokedStatus revoked) + { + logger.log(Level.FINER, "OCSP status is REVOKED as of " + revoked.getRevocationTime()); + status = "REVOKED"; + ocsp_confirmation = true; + } + else + { // Allow PENDING etc. but correct VALID + logger.log(Level.FINER, "OCSP status is UNKNOWN"); + if ("VALID".equals(status)) + status = "UNKNOWN"; + } + } + + // Downgrade an unconfirmed VALID, but keep PENDING etc. + if (! ocsp_confirmation && "VALID".equals(status)) + status = "UNKNOWN"; } - else - logger.log(Level.WARNING, pv + " failed to send status, got " + data); + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot decode OCSP response for " + pv.getName(), ex); + status = "ERROR"; + } + + logger.log(Level.FINE, () -> "Effective " + channel.getName() + " = " + status); + + + // Notify listeners + for (var listener : listeners) + listener.handleCertificateStatusUpdate(this); } /** Close the CERT:STATUS:... PV check */ @@ -177,25 +294,28 @@ public static synchronized CertificateStatusMonitor instance() return instance; } - /** @param status_pv_name CERT:STATUS:... PV name - * @param peer_name Name of the peer (principal of the certificate) + /** @param tls_info {@link TLSHandshakeInfo}: certificate, CERT:STATUS:... PV name * @param listener Listener to invoke for certificate status updates * @return {@link CertificateStatus} to which we're subscribed, need to unsubscribe when no longer needed */ - public synchronized CertificateStatus checkCertStatus(final String status_pv_name, final String peer_name, final CertificateStatusListener listener) + public synchronized CertificateStatus checkCertStatus(final TLSHandshakeInfo tls_info,final CertificateStatusListener listener) { - if (!status_pv_name.startsWith("CERT:STATUS:")) + if (!tls_info.status_pv_name.startsWith("CERT:STATUS:")) throw new IllegalArgumentException("Need CERT:STATUS:... PV"); - logger.log(Level.FINER, () -> "Checking " + status_pv_name + " for '" + peer_name + "'"); + logger.log(Level.FINER, () -> "Checking " + tls_info.status_pv_name + " for '" + tls_info.name + "'"); - CertificateStatus cert_stat = certificate_states.computeIfAbsent(status_pv_name, - stat_pv_name -> new CertificateStatus(peer_name, status_pv_name)); + CertificateStatus cert_stat = certificate_states.computeIfAbsent(tls_info.status_pv_name, + stat_pv_name -> new CertificateStatus(tls_info.peer_cert, tls_info.status_pv_name)); cert_stat.addListener(listener); return cert_stat; } + /** Unsubscribe from certificate status updates + * @param certificate_status Certificate status from which to unsubscribe + * @param listener Listener to cancel + */ public synchronized void remove(final CertificateStatus certificate_status, final CertificateStatusListener listener) { if (certificate_status.removeListener(listener)) diff --git a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java index 092c39ff66..192f76df0b 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java +++ b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java @@ -17,6 +17,9 @@ import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import javax.naming.ldap.LdapName; @@ -42,16 +45,23 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class SecureSockets { /** Supported protocols. PVXS prefers 1.3 */ private static final String[] PROTOCOLS = new String[] { "TLSv1.3"}; + /** Initialize only once */ private static boolean initialized = false; + + /** Factory for secure server sockets */ private static SSLServerSocketFactory tls_server_sockets; + + /** Factory for secure client sockets */ private static SSLSocketFactory tls_client_sockets; + /** X509 certificates loaded from the keychain mapped by principal name of the certificate */ + public static Map keychain_x509_certificates = new ConcurrentHashMap<>(); + /** @param keychain_setting "/path/to/keychain;password" * @return {@link SSLContext} with 'keystore' and 'truststore' set to content of keystore * @throws Exception on error @@ -74,11 +84,40 @@ private static SSLContext createContext(final String keychain_setting) throws Ex pass = "".toCharArray(); } - logger.log(Level.CONFIG, () -> "Loading keychain '" + path + "'"); + logger.log(Level.FINE, () -> "Loading keychain '" + path + "'"); final KeyStore key_store = KeyStore.getInstance("PKCS12"); key_store.load(new FileInputStream(path), pass); + // Track each loaded certificate by its principal name + for (String alias : Collections.list(key_store.aliases())) + { + if (key_store.isCertificateEntry(alias)) + { + final Certificate cert = key_store.getCertificate(alias); + if (cert instanceof X509Certificate x509) + { + final String principal = x509.getSubjectX500Principal().toString(); + logger.log(Level.FINE, "Keychain alias '" + alias + "' is X509 certificate for " + principal); + keychain_x509_certificates.put(principal, x509); + // Could print 'cert', but jdk.event.security logger already does that at FINE level + } + } + if (key_store.isKeyEntry(alias)) + { + // final Key key = key_store.getKey(alias, pass); + final Certificate cert = key_store.getCertificate(alias); + if (cert instanceof X509Certificate x509) + { + final String principal = x509.getSubjectX500Principal().toString(); + logger.log(Level.FINE, "Keychain alias '" + alias + "' is X509 key and certificate for " + principal); + keychain_x509_certificates.put(principal, x509); + } + // Could print 'key', but jdk.event.security logger already logs the cert at FINE level + // and logging the key would show the private key + } + } + final KeyManagerFactory key_manager = KeyManagerFactory.getInstance("PKIX"); key_manager.init(key_store, pass); @@ -270,14 +309,25 @@ public static String getPrincipalCN(final Principal principal) /** Information from TLS socket handshake */ public static class TLSHandshakeInfo { + /** Certificate of the peer */ + public final X509Certificate peer_cert; + /** Name by which the peer identified */ - public String name; + public final String name; /** Host of the peer */ - public String hostname; + public final String hostname; /** PV for client certificate status */ - public String status_pv_name; + public final String status_pv_name; + + TLSHandshakeInfo(final X509Certificate peer_cert, final String name, final String hostname, final String status_pv_name) + { + this.peer_cert = peer_cert; + this.name = name; + this.hostname = hostname; + this.status_pv_name = status_pv_name; + } @Override public String toString() @@ -305,6 +355,7 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti try { // Log certificate chain, grep cert status PV name + X509Certificate peer_cert = null; String status_pv_name = ""; final SSLSession session = socket.getSession(); logger.log(Level.FINER, "Client name: '" + SecureSockets.getPrincipalCN(session.getPeerPrincipal()) + "'"); @@ -331,10 +382,12 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti logger.log(Level.FINER, " - Status PV: '" + pv_name + "'"); if (is_principal_cert && pv_name != null && !pv_name.isBlank()) + { + peer_cert = x509; status_pv_name = pv_name; + } } - // No way to check if there is peer info (certificates, principal, ...) // other then success vs. exception.. final Principal principal = session.getPeerPrincipal(); @@ -345,10 +398,7 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti name = principal.getName(); } - final TLSHandshakeInfo info = new TLSHandshakeInfo(); - info.name = name; - info.hostname = socket.getInetAddress().getHostName(); - info.status_pv_name = status_pv_name; + final TLSHandshakeInfo info = new TLSHandshakeInfo(peer_cert, name, socket.getInetAddress().getHostName(), status_pv_name); return info; } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java index f20dda7d46..9d15da070a 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java @@ -98,7 +98,7 @@ public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHa // ServerPV.setWritable will send updated AccessRightsChange }; if (tls_info != null && !tls_info.status_pv_name.isEmpty()) - certificate_status = CertificateStatusMonitor.instance().checkCertStatus(tls_info.status_pv_name, tls_info.name, certificate_status_listener); + certificate_status = CertificateStatusMonitor.instance().checkCertStatus(tls_info, certificate_status_listener); startReceiver(); startSender(); diff --git a/dependencies/phoebus-target/pom.xml b/dependencies/phoebus-target/pom.xml index cb3195b61d..3ddafa143f 100644 --- a/dependencies/phoebus-target/pom.xml +++ b/dependencies/phoebus-target/pom.xml @@ -530,6 +530,18 @@ 3.1.0 + + + org.bouncycastle + bcpkix-jdk18on + 1.82 + + + org.bouncycastle + bcprov-jdk18on + 1.82 + + org.apache.poi From a72a3a2c530ae336fea5c2481e61773e07902a59 Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 5 Nov 2025 11:37:27 -0500 Subject: [PATCH 5/7] Extract CertificateStatus from CertificateStatusMonitor --- .../epics/pva/common/CertificateStatus.java | 244 ++++++++++++++++++ .../pva/common/CertificateStatusListener.java | 3 +- .../pva/common/CertificateStatusMonitor.java | 224 +--------------- .../epics/pva/server/ServerTCPHandler.java | 2 +- 4 files changed, 252 insertions(+), 221 deletions(-) create mode 100644 core/pva/src/main/java/org/epics/pva/common/CertificateStatus.java diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatus.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatus.java new file mode 100644 index 0000000000..b1ec25b3e4 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatus.java @@ -0,0 +1,244 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.common; + +import static org.epics.pva.PVASettings.logger; + +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Date; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; + +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import org.epics.pva.client.ClientChannelState; +import org.epics.pva.client.PVAChannel; +import org.epics.pva.client.PVAClient; +import org.epics.pva.data.Hexdump; +import org.epics.pva.data.PVAByteArray; +import org.epics.pva.data.PVAStructure; +import org.epics.pva.data.nt.PVAEnum; + +/** Certificate status: Valid or not? + * + * Subscribes to the CERT:STATUS:... PV + * - if one is listed on the certificate. + * + * Gets the overall PENDING/VALID/REVOKED/... status + * from the PV and double-checks VALID state via + * the OCSP response bundled in CERT:STATUS:.. updates/ + * + * @author Kay Kasemir + */ +public class CertificateStatus +{ + private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); + private final X509Certificate certificate; + private final String peer_name; + private final PVAChannel pv; + private String status = null; + + /** Called by {@link CertificateStatusMonitor} + * + * @param client {@link PVAClient} for reading CERT:STATUS:.. PV + * @param certificate Certificate to check + * @param status_pv_name CERT:STATUS:.. PV listed on the certificate + */ + CertificateStatus(final PVAClient client, final X509Certificate certificate, final String status_pv_name) + { + this.certificate = certificate; + this.peer_name = certificate.getSubjectX500Principal().getName(); + pv = client.getChannel(status_pv_name, this::handleConnection); + } + + /** @return CERT:STATUS:... PV name */ + public String getPVName() + { + return pv.getName(); + } + + /** @param listener Listener to add (with initial update) */ + void addListener(final CertificateStatusListener listener) + { + listeners.add(listener); + // Send initial update + logger.log(Level.FINER, "Initial " + getPVName() + " update"); + listener.handleCertificateStatusUpdate(this); + } + + /** @param listener Listener to remove + * @return Was that the last listener, can CertificateStatus be removed? + */ + boolean removeListener(final CertificateStatusListener listener) + { + if (! listeners.remove(listener)) + throw new IllegalStateException("Unknown CertificateStatusListener"); + return listeners.isEmpty(); + } + + /** @return Is the certificate currently valid? */ + public boolean isValid() + { + return "VALID".equals(status); + } + + private void handleConnection(final PVAChannel channel, final ClientChannelState state) + { + if (state == ClientChannelState.CONNECTED) + try + { + channel.subscribe("", this::handleMonitor); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot subscribe to " + pv, ex); + } + } + + private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, final PVAStructure data) + { + // Check overall status enum: VALID or UNKNOWN, PENDING, REVOKED, ... + final PVAEnum value = PVAEnum.fromStructure(data.get("value")); + if (value != null) + status = value.enumString(); + else + status = "UNKNOWN"; + logger.log(Level.FINE, () -> "Received " + channel.getName() + " = " + status); + logger.log(Level.FINER, () -> data.toString()); + + try + { + // Check OCSP Response bundled in the PVA structure + final PVAByteArray raw = data.get("ocsp_response"); + if (raw == null) + throw new Exception("Missing 'ocsp_response' in " + data); + + // Is it a successful OCSP response ... + final OCSPResp ocsp_response = new OCSPResp(raw.get()); + if (ocsp_response.getStatus() != OCSPResp.SUCCESSFUL) + throw new Exception("OCSP Response status " + ocsp_response.getStatus()); + // ...with "basic" info? + if (! (ocsp_response.getResponseObject() instanceof BasicOCSPResp)) + throw new Exception("Expected BasicOCSPResp, got " + ocsp_response.getResponseObject()); + final BasicOCSPResp basic = (BasicOCSPResp) ocsp_response.getResponseObject(); + logger.log(Level.FINER, () -> "OCSP responder " + basic.getResponderId().toASN1Primitive().getName()); + + // Validate against certificates in our key chain + boolean valid = false; + for (X509Certificate x509 : SecureSockets.keychain_x509_certificates.values()) + if (basic.isSignatureValid(new JcaContentVerifierProviderBuilder().build(x509))) + { + logger.log(Level.FINER, () -> "OCSP response verified by " + x509.getSubjectX500Principal()); + valid = true; + break; + } + if (! valid) + throw new Exception("Cannot validate OCSP response"); + + // AuthorityKeyIdentifier, public key of "EPICS Root Certificate Authority" + final JcaX509CertificateHolder bc_cert = new JcaX509CertificateHolder(certificate); + final byte[] authority_key_id = AuthorityKeyIdentifier.fromExtensions(bc_cert.getExtensions()).getKeyIdentifierOctets(); + if (authority_key_id == null) + throw new Exception("Cannot get AuthorityKeyIdentifier from " + certificate); + + // OCSP can include one or more responses. Find one that confirms the certificate + boolean ocsp_confirmation = false; + for (SingleResp response : basic.getResponses()) + { + // Is response for the certificate we want to check? + // Same authority? + final CertificateID id = response.getCertID(); + if (! Arrays.equals(authority_key_id, id.getIssuerKeyHash())) + { + logger.log(Level.FINER, () -> "OCSP authority\n" + Hexdump.toHexdump(id.getIssuerKeyHash()) + + "\ndiffers from\n" + Hexdump.toHexdump(authority_key_id)); + continue; + } + + // Same serial number? + if (! id.getSerialNumber().equals(certificate.getSerialNumber())) + { + logger.log(Level.FINER, () -> "OCSP Serial: 0x" + id.getSerialNumber().toString(16) + + " differs from expected 0x" + certificate.getSerialNumber().toString(16)); + continue; + } + + // Is covered time range from <= now <= until? 'until' may be null... + final Date now = new Date(), from = response.getThisUpdate(), until = response.getNextUpdate(); + if (from.after(now) || (until != null && now.after(until))) + { + logger.log(Level.FINER, () -> "Applicable time range " + from + " to " + until + + " does not include now, " + now); + continue; + } + + // Seems applicable to the certificate we want to check. + + // What is the status? OCSP only indicates null for valid, RevokedStatus with revocation date, or UnknownStatus. + // Use that to potentially correct the more detailed status from the enum + final org.bouncycastle.cert.ocsp.CertificateStatus response_status = response.getCertStatus(); + if (response_status == org.bouncycastle.cert.ocsp.CertificateStatus.GOOD) + { + logger.log(Level.FINER, "OCSP status is VALID"); + status = "VALID"; + ocsp_confirmation = true; + break; + } + else if (response_status instanceof RevokedStatus revoked) + { + logger.log(Level.FINER, "OCSP status is REVOKED as of " + revoked.getRevocationTime()); + status = "REVOKED"; + ocsp_confirmation = true; + } + else + { // Allow PENDING etc. but correct VALID + logger.log(Level.FINER, "OCSP status is UNKNOWN"); + if ("VALID".equals(status)) + status = "UNKNOWN"; + } + } + + // Downgrade an unconfirmed VALID, but keep PENDING etc. + if (! ocsp_confirmation && "VALID".equals(status)) + status = "UNKNOWN"; + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot decode OCSP response for " + pv.getName(), ex); + status = "ERROR"; + } + + logger.log(Level.FINE, () -> "Effective " + channel.getName() + " = " + status); + + // Notify listeners + for (var listener : listeners) + listener.handleCertificateStatusUpdate(this); + } + + /** Close the CERT:STATUS:... PV check */ + void close() + { + if (! listeners.isEmpty()) + throw new IllegalStateException(getPVName() + " is still in use"); + pv.close(); + } + + @Override + public String toString() + { + return pv.getName() + " for '" + peer_name + "' is " + status; + } +} \ No newline at end of file diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java index fbdc8ced9d..177392e71d 100644 --- a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java @@ -5,7 +5,6 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html ******************************************************************************/ - package org.epics.pva.common; /** Listener to certificate status updates @@ -14,5 +13,5 @@ public interface CertificateStatusListener { /** @param update Certificate status update */ - public void handleCertificateStatusUpdate(CertificateStatusMonitor.CertificateStatus update); + public void handleCertificateStatusUpdate(CertificateStatus update); } diff --git a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java index fa6e8662ad..ed5b6155ba 100644 --- a/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java +++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java @@ -5,35 +5,15 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html ******************************************************************************/ - package org.epics.pva.common; import static org.epics.pva.PVASettings.logger; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Date; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; -import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import org.bouncycastle.cert.ocsp.BasicOCSPResp; -import org.bouncycastle.cert.ocsp.CertificateID; -import org.bouncycastle.cert.ocsp.OCSPResp; -import org.bouncycastle.cert.ocsp.RevokedStatus; -import org.bouncycastle.cert.ocsp.SingleResp; -import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; -import org.epics.pva.client.ClientChannelState; -import org.epics.pva.client.PVAChannel; import org.epics.pva.client.PVAClient; import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; -import org.epics.pva.data.Hexdump; -import org.epics.pva.data.PVAByteArray; -import org.epics.pva.data.PVAStructure; -import org.epics.pva.data.nt.PVAEnum; /** Monitors the 'CERT:STATUS:...' PV for a certificate * @@ -60,6 +40,10 @@ */ public class CertificateStatusMonitor { + // Most of the work is done in the CertificateStatus. + // This class holds the common PVAClient and handles + // the synchronization of creating and removing cert status checks. + /** Singleton instance */ private static CertificateStatusMonitor instance = null; @@ -69,202 +53,6 @@ public class CertificateStatusMonitor /** PVA Client used for all CERT:STATUS:... PVs */ private PVAClient client = null; - /** Certificate status: Valid or not? */ - public class CertificateStatus - { - private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); - private final X509Certificate certificate; - private final String peer_name; - private final PVAChannel pv; - private String status = null; - - CertificateStatus(final X509Certificate certificate, final String status_pv_name) - { - this.certificate = certificate; - this.peer_name = certificate.getSubjectX500Principal().getName(); - pv = client.getChannel(status_pv_name, this::handleConnection); - } - - /** @return CERT:STATUS:... PV name */ - public String getPVName() - { - return pv.getName(); - } - - /** @param listener Listener to add (with initial update) */ - void addListener(final CertificateStatusListener listener) - { - listeners.add(listener); - // Send initial update - logger.log(Level.FINER, "Initial " + getPVName() + " update"); - listener.handleCertificateStatusUpdate(this); - } - - /** @param listener Listener to remove - * @return Was that the last listener, can CertificateStatus be removed? - */ - boolean removeListener(final CertificateStatusListener listener) - { - if (! listeners.remove(listener)) - throw new IllegalStateException("Unknown CertificateStatusListener"); - return listeners.isEmpty(); - } - - /** @return Is the certificate currently valid? */ - public boolean isValid() - { - return "VALID".equals(status); - } - - private void handleConnection(final PVAChannel channel, final ClientChannelState state) - { - if (state == ClientChannelState.CONNECTED) - try - { - channel.subscribe("", this::handleMonitor); - } - catch (Exception ex) - { - logger.log(Level.WARNING, "Cannot subscribe to " + pv, ex); - } - } - - private void handleMonitor(final PVAChannel channel, final BitSet changes, final BitSet overruns, final PVAStructure data) - { - // Check overall status enum: VALID or UNKNOWN, PENDING, REVOKED, ... - final PVAEnum value = PVAEnum.fromStructure(data.get("value")); - if (value != null) - status = value.enumString(); - else - status = "UNKNOWN"; - logger.log(Level.FINE, () -> "Received " + channel.getName() + " = " + status); - logger.log(Level.FINER, () -> data.toString()); - - try - { - // Check OCSP Response bundled in the PVA structure - final PVAByteArray raw = data.get("ocsp_response"); - if (raw == null) - throw new Exception("Missing 'ocsp_response' in " + data); - - // Is it a successful OCSP response ... - final OCSPResp ocsp_response = new OCSPResp(raw.get()); - if (ocsp_response.getStatus() != OCSPResp.SUCCESSFUL) - throw new Exception("OCSP Response status " + ocsp_response.getStatus()); - // ...with "basic" info? - if (! (ocsp_response.getResponseObject() instanceof BasicOCSPResp)) - throw new Exception("Expected BasicOCSPResp, got " + ocsp_response.getResponseObject()); - final BasicOCSPResp basic = (BasicOCSPResp) ocsp_response.getResponseObject(); - logger.log(Level.FINER, () -> "OCSP responder " + basic.getResponderId().toASN1Primitive().getName()); - - // Validate against certificates in our key chain - boolean valid = false; - for (X509Certificate x509 : SecureSockets.keychain_x509_certificates.values()) - if (basic.isSignatureValid(new JcaContentVerifierProviderBuilder().build(x509))) - { - logger.log(Level.FINER, () -> "OCSP response verified by " + x509.getSubjectX500Principal()); - valid = true; - break; - } - if (! valid) - throw new Exception("Cannot validate OCSP response"); - - // AuthorityKeyIdentifier, public key of "EPICS Root Certificate Authority" - final JcaX509CertificateHolder bc_cert = new JcaX509CertificateHolder(certificate); - final byte[] authority_key_id = AuthorityKeyIdentifier.fromExtensions(bc_cert.getExtensions()).getKeyIdentifierOctets(); - if (authority_key_id == null) - throw new Exception("Cannot get AuthorityKeyIdentifier from " + certificate); - - // OCSP can include one or more responses. Find one that confirms the certificate - boolean ocsp_confirmation = false; - for (SingleResp response : basic.getResponses()) - { - // Is response for the certificate we want to check? - // Same authority? - final CertificateID id = response.getCertID(); - if (! Arrays.equals(authority_key_id, id.getIssuerKeyHash())) - { - logger.log(Level.FINER, () -> "OCSP authority\n" + Hexdump.toHexdump(id.getIssuerKeyHash()) + - "\ndiffers from\n" + Hexdump.toHexdump(authority_key_id)); - continue; - } - - // Same serial number? - if (! id.getSerialNumber().equals(certificate.getSerialNumber())) - { - logger.log(Level.FINER, () -> "OCSP Serial: 0x" + id.getSerialNumber().toString(16) + - " differs from expected 0x" + certificate.getSerialNumber().toString(16)); - continue; - } - - // Is applicable time range from <= now <= until? until may be null... - final Date now = new Date(), from = response.getThisUpdate(), until = response.getNextUpdate(); - if (from.after(now) || (until != null && now.after(until))) - { - logger.log(Level.FINER, () -> "Applicable time range from " + response.getThisUpdate() + - " to " + response.getNextUpdate() + " does not include now, " + now); - continue; - } - - // Seems to apply to the certificate we want to check. - - // What is the status? OCSP only indicates null for valid, RevokedStatus with revocation date, or UnknownStatus - // Use that to potentially update the more detailed - final org.bouncycastle.cert.ocsp.CertificateStatus response_status = response.getCertStatus(); - if (response_status == org.bouncycastle.cert.ocsp.CertificateStatus.GOOD) - { - logger.log(Level.FINER, "OCSP status is VALID"); - status = "VALID"; - ocsp_confirmation = true; - break; - } - else if (response_status instanceof RevokedStatus revoked) - { - logger.log(Level.FINER, "OCSP status is REVOKED as of " + revoked.getRevocationTime()); - status = "REVOKED"; - ocsp_confirmation = true; - } - else - { // Allow PENDING etc. but correct VALID - logger.log(Level.FINER, "OCSP status is UNKNOWN"); - if ("VALID".equals(status)) - status = "UNKNOWN"; - } - } - - // Downgrade an unconfirmed VALID, but keep PENDING etc. - if (! ocsp_confirmation && "VALID".equals(status)) - status = "UNKNOWN"; - } - catch (Exception ex) - { - logger.log(Level.WARNING, "Cannot decode OCSP response for " + pv.getName(), ex); - status = "ERROR"; - } - - logger.log(Level.FINE, () -> "Effective " + channel.getName() + " = " + status); - - - // Notify listeners - for (var listener : listeners) - listener.handleCertificateStatusUpdate(this); - } - - /** Close the CERT:STATUS:... PV check */ - void close() - { - if (! listeners.isEmpty()) - throw new IllegalStateException(getPVName() + " is still in use"); - pv.close(); - } - - @Override - public String toString() - { - return pv.getName() + " for '" + peer_name + "' is " + status; - } - } - /** Constructor of the singleton instance */ private CertificateStatusMonitor() { @@ -274,7 +62,7 @@ private CertificateStatusMonitor() } catch (Exception ex) { - logger.log(Level.WARNING, "Cannot create PVAClient for CERT:STATUS:... monitor", ex); + logger.log(Level.SEVERE, "Cannot create PVAClient for CERT:STATUS:... monitor", ex); } } @@ -306,7 +94,7 @@ public synchronized CertificateStatus checkCertStatus(final TLSHandshakeInfo tls logger.log(Level.FINER, () -> "Checking " + tls_info.status_pv_name + " for '" + tls_info.name + "'"); CertificateStatus cert_stat = certificate_states.computeIfAbsent(tls_info.status_pv_name, - stat_pv_name -> new CertificateStatus(tls_info.peer_cert, tls_info.status_pv_name)); + stat_pv_name -> new CertificateStatus(client, tls_info.peer_cert, tls_info.status_pv_name)); cert_stat.addListener(listener); return cert_stat; diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java index 9d15da070a..76d160eec8 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java @@ -15,9 +15,9 @@ import java.util.Objects; import java.util.logging.Level; +import org.epics.pva.common.CertificateStatus; import org.epics.pva.common.CertificateStatusListener; import org.epics.pva.common.CertificateStatusMonitor; -import org.epics.pva.common.CertificateStatusMonitor.CertificateStatus; import org.epics.pva.common.CommandHandlers; import org.epics.pva.common.PVAAuth; import org.epics.pva.common.PVAHeader; From 78d6b1811b4b3a789923c8e18bc42a637360c8ad Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 5 Nov 2025 11:39:39 -0500 Subject: [PATCH 6/7] Eclipse dependencies for bouncycastle --- dependencies/phoebus-target/.classpath | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dependencies/phoebus-target/.classpath b/dependencies/phoebus-target/.classpath index 6d8c96f408..ae801fe781 100644 --- a/dependencies/phoebus-target/.classpath +++ b/dependencies/phoebus-target/.classpath @@ -2,8 +2,12 @@ + + + + @@ -58,6 +62,7 @@ + @@ -111,8 +116,8 @@ - - + + From f59456e28ba4d27e9b306a4d95ee74b7a7dbb735 Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 5 Nov 2025 11:55:50 -0500 Subject: [PATCH 7/7] serverdemo: Include bouncycastle --- core/pva/serverdemo | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/pva/serverdemo b/core/pva/serverdemo index f6ee0de6d5..88b2a7f914 100755 --- a/core/pva/serverdemo +++ b/core/pva/serverdemo @@ -1,11 +1,14 @@ #!/bin/sh +LIB=../../dependencies/phoebus-target/target/lib/bcpkix-jdk18on-1.82.jar:../../dependencies/phoebus-target/target/lib/bcprov-jdk18on-1.82.jar + JAR=`echo target/core-pva*.jar` if [ -r "$JAR" ] then # Echo use jar file - java -cp $JAR org.epics.pva.server.ServerDemo "$@" + java -cp $LIB:$JAR org.epics.pva.server.ServerDemo "$@" else # Use build output - java -cp target/classes org.epics.pva.server.ServerDemo "$@" + java -cp $LIB:target/classes org.epics.pva.server.ServerDemo "$@" fi +