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 trusted certificate for " +
+ 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?
+ final CertificateID id = response.getCertID();
+
+ // 1) Same authority name (only have hash of name)?
+ // When last checked, hash_alg was "1.3.14.3.2.26" = SHA-1
+ final String hash_alg = id.getHashAlgOID().getId();
+ final MessageDigest digest = MessageDigest.getInstance(hash_alg);
+ final byte[] cert_issuer_name_hash = digest.digest(bc_cert.getIssuer().getEncoded());
+ if (! Arrays.equals(cert_issuer_name_hash, id.getIssuerNameHash()))
+ {
+ logger.log(Level.FINER, () -> "OCSP authority hash for name " + certificate.getIssuerX500Principal() +
+ "\n" + Hexdump.toHexdump(id.getIssuerNameHash()) +
+ "\ndiffers from expected\n" + Hexdump.toHexdump(cert_issuer_name_hash));
+ continue;
+ }
+ logger.log(Level.FINER, () -> "OCSP authority hash for name " + certificate.getIssuerX500Principal() +
+ "\n" + Hexdump.toHexdump(id.getIssuerNameHash()));
+
+ // 2) Same authority key?
+ if (! Arrays.equals(authority_key_id, id.getIssuerKeyHash()))
+ {
+ logger.log(Level.FINER, () -> "OCSP authority key\n" + Hexdump.toHexdump(id.getIssuerKeyHash()) +
+ "\ndiffers from expected\n" + Hexdump.toHexdump(authority_key_id));
+ continue;
+ }
+ logger.log(Level.FINER, () -> "OCSP authority key\n" + Hexdump.toHexdump(id.getIssuerKeyHash()));
+
+ // 3) 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;
+ }
+ logger.log(Level.FINER, () -> "OCSP Serial: 0x" + id.getSerialNumber().toString(16));
+
+ // Response seems applicable to the certificate we want to check!
+
+ // 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;
+ }
+
+ // 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
new file mode 100644
index 0000000000..177392e71d
--- /dev/null
+++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusListener.java
@@ -0,0 +1,17 @@
+/*******************************************************************************
+ * 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(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..ed5b6155ba
--- /dev/null
+++ b/core/pva/src/main/java/org/epics/pva/common/CertificateStatusMonitor.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * 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.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+
+import org.epics.pva.client.PVAClient;
+import org.epics.pva.common.SecureSockets.TLSHandshakeInfo;
+
+/** 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
+{
+ // 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;
+
+ /** 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;
+
+ /** Constructor of the singleton instance */
+ private CertificateStatusMonitor()
+ {
+ try
+ {
+ client = new PVAClient();
+ }
+ catch (Exception ex)
+ {
+ logger.log(Level.SEVERE, "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 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 TLSHandshakeInfo tls_info,final CertificateStatusListener listener)
+ {
+ if (!tls_info.status_pv_name.startsWith("CERT:STATUS:"))
+ throw new IllegalArgumentException("Need CERT:STATUS:... PV");
+
+ 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(client, 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))
+ {
+ 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/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/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/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..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);
}
@@ -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/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 57a1f890ac..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
@@ -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();
}
@@ -241,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;
}
@@ -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/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
+ {
+ 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..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,6 +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.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,19 +58,48 @@ 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
{
super(false);
+
+ logger.log(Level.FINER, () -> "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);
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, certificate_status_listener);
+
startReceiver();
startSender();
@@ -106,10 +137,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 +174,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/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
{
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);
}
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 @@
-
-
+
+
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