From b4f15c1f7ea72190c7b2ab35e05d364b21259169 Mon Sep 17 00:00:00 2001 From: george-mcintyre Date: Wed, 15 Apr 2026 13:27:54 +0200 Subject: [PATCH 1/2] Add keychain password file support --- .../org/epics/pva/common/SecureSockets.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) 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 d15d54957b..e735755678 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 @@ -14,6 +14,8 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyStore; import java.security.Principal; import java.security.cert.Certificate; @@ -63,16 +65,18 @@ public class SecureSockets /** 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" + /** @param keychain_setting "/path/to/keychain", "/path/to/keychain;password", + * or just "/path/to/keychain" with password in a separate *_PWD_FILE + * @param is_server true for server keychain (uses EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE), + * false for client (uses EPICS_PVA_TLS_KEYCHAIN_PWD_FILE) * @return {@link SSLContext} with 'keystore' and 'truststore' set to content of keystore * @throws Exception on error */ - private static SSLContext createContext(final String keychain_setting) throws Exception + private static SSLContext createContext(final String keychain_setting, final boolean is_server) throws Exception { final String path; final char[] pass; - // We support the default "" empty as well as actual passwords, but not null for no password final int sep = keychain_setting.indexOf(';'); if (sep > 0) { @@ -82,7 +86,7 @@ private static SSLContext createContext(final String keychain_setting) throws Ex else { path = keychain_setting; - pass = "".toCharArray(); + pass = readKeychainPassword(is_server); } logger.log(Level.FINE, () -> "Loading keychain '" + path + "'"); @@ -131,6 +135,29 @@ private static SSLContext createContext(final String keychain_setting) throws Ex return context; } + private static char[] readKeychainPassword(final boolean is_server) + { + final String env_name = is_server ? "EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE" + : "EPICS_PVA_TLS_KEYCHAIN_PWD_FILE"; + final String pwd_file = PVASettings.get(env_name, ""); + if (! pwd_file.isEmpty()) + { + try + { + final String password = Files.readString(Path.of(pwd_file)).trim(); + logger.log(Level.FINE, () -> "Read keychain password from " + pwd_file); + return password.toCharArray(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Error reading password file " + pwd_file, ex); + } + } + // Java PKCS12: null skips encrypted sections (loses CA certs). + // Empty array attempts decryption with retry via NUL char fallback. + return new char[0]; + } + private static synchronized void initialize() throws Exception { if (initialized) @@ -138,13 +165,13 @@ private static synchronized void initialize() throws Exception if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank()) { - final SSLContext context = createContext(PVASettings.EPICS_PVAS_TLS_KEYCHAIN); + final SSLContext context = createContext(PVASettings.EPICS_PVAS_TLS_KEYCHAIN, true); tls_server_sockets = context.getServerSocketFactory(); } if (! PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank()) { - final SSLContext context = createContext(PVASettings.EPICS_PVA_TLS_KEYCHAIN); + final SSLContext context = createContext(PVASettings.EPICS_PVA_TLS_KEYCHAIN, false); tls_client_sockets = context.getSocketFactory(); } initialized = true; From cd72fb5afb6b1aaeab07278942091cb8ef781792 Mon Sep 17 00:00:00 2001 From: george-mcintyre Date: Wed, 15 Apr 2026 17:56:13 +0200 Subject: [PATCH 2/2] Declare and document EPICS_PVA(S)_TLS_KEYCHAIN_PWD_FILE in PVASettings Per review feedback: all PVA settings must be declared as public static fields in PVASettings.java, documented next to their related keychain settings, and initialised in the static block. - Add EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE next to EPICS_PVAS_TLS_KEYCHAIN - Add EPICS_PVA_TLS_KEYCHAIN_PWD_FILE next to EPICS_PVA_TLS_KEYCHAIN - Initialise both in the static block alongside their keychain peers - Update SecureSockets.readKeychainPassword() to read from the PVASettings fields rather than calling PVASettings.get() directly --- .../main/java/org/epics/pva/PVASettings.java | 34 +++++++++++++++++++ .../org/epics/pva/common/SecureSockets.java | 5 ++- 2 files changed, 36 insertions(+), 3 deletions(-) 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 0c36568ede..6c9ffeecc2 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -153,6 +153,22 @@ public class PVASettings */ public static String EPICS_PVAS_TLS_KEYCHAIN = ""; + /** Path to a file containing the password for the PVA server keychain. + * + *

Alternative to embedding the password in {@link #EPICS_PVAS_TLS_KEYCHAIN} + * using the "/path/to/file;password" syntax. + * When set, the password is read from this file instead, with leading and trailing + * whitespace stripped. + * Takes precedence over an inline password in {@link #EPICS_PVAS_TLS_KEYCHAIN} + * when no ";" separator is present in that setting. + * + *

Intended for environments where secrets are mounted as files, + * for example Kubernetes pods using a {@code Secret} volume. + * + *

When empty, no password file is used. + */ + public static String EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE = ""; + /** Secure server options * *

    @@ -183,6 +199,22 @@ public class PVASettings */ public static String EPICS_PVA_TLS_KEYCHAIN = ""; + /** Path to a file containing the password for the PVA client keychain. + * + *

    Alternative to embedding the password in {@link #EPICS_PVA_TLS_KEYCHAIN} + * using the "/path/to/file;password" syntax. + * When set, the password is read from this file instead, with leading and trailing + * whitespace stripped. + * Takes precedence over an inline password in {@link #EPICS_PVA_TLS_KEYCHAIN} + * when no ";" separator is present in that setting. + * + *

    Intended for environments where secrets are mounted as files, + * for example Kubernetes pods using a {@code Secret} volume. + * + *

    When empty, no password file is used. + */ + public static String EPICS_PVA_TLS_KEYCHAIN_PWD_FILE = ""; + /** TCP buffer size for sending data * *

    Messages are constructed within this buffer, @@ -281,9 +313,11 @@ public class PVASettings EPICS_PVA_TCP_SOCKET_TMO = get("EPICS_PVA_TCP_SOCKET_TMO", EPICS_PVA_TCP_SOCKET_TMO); EPICS_PVA_MAX_ARRAY_FORMATTING = get("EPICS_PVA_MAX_ARRAY_FORMATTING", EPICS_PVA_MAX_ARRAY_FORMATTING); EPICS_PVAS_TLS_KEYCHAIN = get("EPICS_PVAS_TLS_KEYCHAIN", EPICS_PVAS_TLS_KEYCHAIN); + EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE = get("EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE", EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE); EPICS_PVAS_TLS_OPTIONS = get("EPICS_PVAS_TLS_OPTIONS", EPICS_PVAS_TLS_OPTIONS); require_client_cert = EPICS_PVAS_TLS_OPTIONS.contains("client_cert=require"); EPICS_PVA_TLS_KEYCHAIN = get("EPICS_PVA_TLS_KEYCHAIN", EPICS_PVA_TLS_KEYCHAIN); + EPICS_PVA_TLS_KEYCHAIN_PWD_FILE = get("EPICS_PVA_TLS_KEYCHAIN_PWD_FILE", EPICS_PVA_TLS_KEYCHAIN_PWD_FILE); if (EPICS_PVA_TLS_KEYCHAIN.isEmpty() && !EPICS_PVAS_TLS_KEYCHAIN.isEmpty()) { EPICS_PVA_TLS_KEYCHAIN = EPICS_PVAS_TLS_KEYCHAIN; 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 e735755678..6f2ab6cd42 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 @@ -137,9 +137,8 @@ private static SSLContext createContext(final String keychain_setting, final boo private static char[] readKeychainPassword(final boolean is_server) { - final String env_name = is_server ? "EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE" - : "EPICS_PVA_TLS_KEYCHAIN_PWD_FILE"; - final String pwd_file = PVASettings.get(env_name, ""); + final String pwd_file = is_server ? PVASettings.EPICS_PVAS_TLS_KEYCHAIN_PWD_FILE + : PVASettings.EPICS_PVA_TLS_KEYCHAIN_PWD_FILE; if (! pwd_file.isEmpty()) { try