Skip to content

Commit

Permalink
EPICS_PVA*_TLS_KEYCHAIN now w/ password;fetch TLS principal (name)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasemir committed Aug 11, 2023
1 parent b4553a6 commit 5d69683
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 57 deletions.
10 changes: 4 additions & 6 deletions core/pva/TLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ Step 3: Configure and run the demo server
Set environment variables to inform the server about its keystore:

```
export EPICS_PVAS_TLS_KEYCHAIN=/path/to/KEYSTORE
export EPICS_PVA_STOREPASS=changeit
export EPICS_PVAS_TLS_KEYCHAIN="/path/to/KEYSTORE;changeit"
```

Then run a demo server
Expand All @@ -85,8 +84,7 @@ Step 4: Configure and run the demo client
Set environment variables to inform the client about its truststore:

```
export EPICS_PVA_TLS_KEYCHAIN=/path/to/TRUSTSTORE
export EPICS_PVA_STOREPASS=changeit
export EPICS_PVA_TLS_KEYCHAIN="/path/to/TRUSTSTORE;changeit"
```

Then run a demo client
Expand Down Expand Up @@ -192,6 +190,6 @@ which needs to be imported into the PKCS12 file format:
keytool -importcert -alias myca -keystore trust_ca.p12 -storepass changeit -file myca.cer -noprompt
```

We can now run the server with `EPICS_PVAS_TLS_KEYCHAIN=/path/to/ioc.p12` and clients with
`EPICS_PVA_TLS_KEYCHAIN=/path/to/trust_ca.p12`, both with `EPICS_PVA_STOREPASS=changeit`
We can now run the server with `EPICS_PVAS_TLS_KEYCHAIN=/path/to/ioc.p12;changeit` and clients with
`EPICS_PVA_TLS_KEYCHAIN=/path/to/trust_ca.p12;changeit`

28 changes: 13 additions & 15 deletions core/pva/src/main/java/org/epics/pva/PVASettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,28 +133,27 @@ public class PVASettings
/** Multicast address used for the local re-send of IPv4 unicasts */
public static String EPICS_PVA_MULTICAST_GROUP = "224.0.0.128";

/** Path to key store, a PKCS12 file that contains server's public and private key.
* When empty, PVA server does not support secure (TLS) communication.
/** Path to PVA server keystore and truststore, a PKCS12 file that contains server's public and private key
* as well as trusted CAs that are used to verify client certificates.
*
* <p>Format: "/path/to/file;password".
*
* <p>When empty, PVA server does not support secure (TLS) communication.
*/
public static String EPICS_PVAS_TLS_KEYCHAIN = "";

/** Path to trust store, a PKCS12 file that contains the certificates or root CA
* that the client will trust.
* When empty, PVA client does not support secure (TLS) communication.
/** Path to PVA client keystore and truststore, a PKCS12 file that contains the certificates or root CA
* that the client will trust when verifying a server certificate,
* and optional client certificate used with x509 authentication to establish the client's name.
*
* <p>Format: "/path/to/file;password".
*
* <p>When empty, PVA client does not support secure (TLS) communication.
* When configured, PVA client can reply to PVA servers that offer "tls" in a search reply,
* and searches via EPICS_PVA_NAME_SERVERS will also use TLS.
*/
public static String EPICS_PVA_TLS_KEYCHAIN = "";

/** Password used to open the EPICS_PVAS_TLS_KEYCHAIN as well as EPICS_PVA_TLS_KEYCHAIN.
* May be empty if key stores do not use a password.
*
* The wrong password will result in an obvious "keystore password was incorrect" exception,
* but an empty password for a keystore that requires one can result in an exception
* with message "the trustAnchors parameter must be non-empty".
*/
public static String EPICS_PVA_STOREPASS = "";

/** TCP buffer size for sending data
*
* <p>Messages are constructed within this buffer,
Expand Down Expand Up @@ -237,7 +236,6 @@ public class PVASettings
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_PVA_TLS_KEYCHAIN = get("EPICS_PVA_TLS_KEYCHAIN", EPICS_PVA_TLS_KEYCHAIN);
EPICS_PVA_STOREPASS = get("EPICS_PVA_STOREPASS", EPICS_PVA_STOREPASS);
EPICS_PVA_SEND_BUFFER_SIZE = get("EPICS_PVA_SEND_BUFFER_SIZE", EPICS_PVA_SEND_BUFFER_SIZE);
EPICS_PVA_FAST_BEACON_MIN = get("EPICS_PVA_FAST_BEACON_MIN", EPICS_PVA_FAST_BEACON_MIN);
EPICS_PVA_FAST_BEACON_MAX = get("EPICS_PVA_FAST_BEACON_MAX", EPICS_PVA_FAST_BEACON_MAX);
Expand Down
130 changes: 98 additions & 32 deletions core/pva/src/main/java/org/epics/pva/common/SecureSockets.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
import java.security.KeyStore;
import java.util.logging.Level;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.epics.pva.PVASettings;
Expand All @@ -30,54 +31,74 @@
* By default, provide plain TCP sockets.
*
* To enable TLS sockets, EPICS_PVAS_TLS_KEYCHAIN can be set to
* select a keystore for the server, and EPICS_PVA_TLS_KEYCHAIN can define
* a trust store for the client.
* The optional password for both is in EPICS_PVA_STOREPASS.
* select a key- and truststore for the server, and EPICS_PVA_TLS_KEYCHAIN can define
* one for the client.
*
* @author Kay Kasemir
*/
@SuppressWarnings("nls")
public class SecureSockets
{
/** Supported protocols. PVXS prefers 1.3 */
private static final String[] PROTOCOLS = new String[] { "TLSv1.3"};

private static boolean initialized = false;
private static ServerSocketFactory tls_server_sockets;
private static SocketFactory tls_client_sockets;
private static SSLServerSocketFactory tls_server_sockets;
private static SSLSocketFactory tls_client_sockets;

private static synchronized void initialize() throws Exception
/** @param keychain_setting "/path/to/keychain;password"
* @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
{
if (initialized)
return;
final String path;
final char[] pass;

// We support the default "" empty as well as actual passwords, but not null for no password
final char[] password = PVASettings.EPICS_PVA_STOREPASS.toCharArray();

if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank())
final int sep = keychain_setting.indexOf(';');
if (sep > 0)
{
logger.log(Level.INFO, "Loading keystore '" + PVASettings.EPICS_PVAS_TLS_KEYCHAIN + "'");
final KeyStore key_store = KeyStore.getInstance("PKCS12");
key_store.load(new FileInputStream(PVASettings.EPICS_PVAS_TLS_KEYCHAIN), password);
path = keychain_setting.substring(0, sep);
pass = keychain_setting.substring(sep+1).toCharArray();
}
else
{
path = keychain_setting;
pass = "".toCharArray();
}

final KeyStore key_store = KeyStore.getInstance("PKCS12");
key_store.load(new FileInputStream(path), pass);

final KeyManagerFactory key_manager = KeyManagerFactory.getInstance("PKIX");
key_manager.init(key_store, pass);

final TrustManagerFactory trust_manager = TrustManagerFactory.getInstance("PKIX");
trust_manager.init(key_store);

final SSLContext context = SSLContext.getInstance("TLS");
context.init(key_manager.getKeyManagers(), trust_manager.getTrustManagers(), null);

final KeyManagerFactory key_manager = KeyManagerFactory.getInstance("PKIX");
key_manager.init(key_store, password);
return context;
}

final SSLContext context = SSLContext.getInstance("TLS");
context.init(key_manager.getKeyManagers(), null, null);
private static synchronized void initialize() throws Exception
{
if (initialized)
return;

if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank())
{
logger.log(Level.INFO, "Loading server keychain '" + PVASettings.EPICS_PVAS_TLS_KEYCHAIN + "'");
final SSLContext context = createContext(PVASettings.EPICS_PVAS_TLS_KEYCHAIN);
tls_server_sockets = context.getServerSocketFactory();
}

if (! PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank())
{
logger.log(Level.INFO, "Loading truststore '" + PVASettings.EPICS_PVA_TLS_KEYCHAIN + "'");
final KeyStore trust_store = KeyStore.getInstance("PKCS12");
trust_store.load(new FileInputStream(PVASettings.EPICS_PVA_TLS_KEYCHAIN), password);

final TrustManagerFactory trust_manager = TrustManagerFactory.getInstance("PKIX");
trust_manager.init(trust_store);

SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trust_manager.getTrustManagers(), null);

logger.log(Level.INFO, "Loading client keychain '" + PVASettings.EPICS_PVA_TLS_KEYCHAIN + "'");
final SSLContext context = createContext(PVASettings.EPICS_PVA_TLS_KEYCHAIN);
tls_client_sockets = context.getSocketFactory();
}
initialized = true;
Expand All @@ -98,6 +119,10 @@ public static ServerSocket createServerSocket(final InetSocketAddress address, f
if (tls_server_sockets == null)
throw new Exception("TLS is not supported. Configure EPICS_PVAS_TLS_KEYCHAIN");
socket = tls_server_sockets.createServerSocket();

// Request, but don't require, client's certificate with 'principal' name for x509 authentication
((SSLServerSocket) socket).setWantClientAuth(true);
((SSLServerSocket) socket).setEnabledProtocols(PROTOCOLS);
}
else
socket = new ServerSocket();
Expand Down Expand Up @@ -130,10 +155,51 @@ public static Socket createClientSocket(final InetSocketAddress address, final b
if (tls_client_sockets == null)
throw new Exception("TLS is not supported. Configure EPICS_PVA_TLS_KEYCHAIN");
final SSLSocket socket = (SSLSocket) tls_client_sockets.createSocket(address.getAddress(), address.getPort());
// PVXS prefers 1.3
socket.setEnabledProtocols(new String[] { "TLSv1.3"});
socket.setEnabledProtocols(PROTOCOLS);
// Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket
socket.startHandshake();
return socket;
}

/** Information from TLS socket handshake */
public static class TLSHandshakeInfo
{
/** Name by which the peer identified */
public String name;

/** Get TLS/SSH info from socket
* @param socket {@link SSLSocket}
* @return {@link TLSHandshakeInfo} or <code>null</code>
* @throws Exception on error
*/
public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Exception
{
// Start ASAP instead of waiting for first read/write on socket.
// "This method is synchronous for the initial handshake on a connection
// and returns when the negotiated handshake is complete",
// so no need to addHandshakeCompletedListener()
socket.startHandshake();

try
{
// No way to check if there is peer info (certificates, principal, ...)
// other then success vs. exception..
String name = socket.getSession().getPeerPrincipal().getName();
if (name.startsWith("CN="))
name = name.substring(3);
else
logger.log(Level.WARNING, "Peer " + socket.getInetAddress() + " sent '" + name + "' as principal name, expected 'CN=...'");
final TLSHandshakeInfo info = new TLSHandshakeInfo();
info.name = name;
return info;
}
catch (Exception ex)
{
// Clients may not have a certificate..
// System.out.println("No x509 name from client");
// ex.printStackTrace();
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.epics.pva.common.PVAHeader;
import org.epics.pva.common.RequestEncoder;
import org.epics.pva.common.SearchResponse;
import org.epics.pva.common.SecureSockets.TLSHandshakeInfo;
import org.epics.pva.common.TCPHandler;
import org.epics.pva.data.PVASize;
import org.epics.pva.data.PVAString;
Expand Down Expand Up @@ -50,16 +51,28 @@ class ServerTCPHandler extends TCPHandler
/** Server that holds all the PVs */
private final PVAServer server;

/** Info from TLS socket handshake or <code>null</code> */
private final TLSHandshakeInfo tls_info;

/** 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;

public ServerTCPHandler(final PVAServer server, final Socket client) throws Exception

public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHandshakeInfo tls_info) throws Exception
{
super(client, false);
this.server = server;
this.tls_info = tls_info;

// TODO Use name for x509 auth
if (this.tls_info != null)
System.out.println("GOT x509 NAME '" + this.tls_info.name + "'!");
else
System.out.println("GOT NO x509 NAME...");

server.register(this);
startSender();

Expand Down
16 changes: 13 additions & 3 deletions core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.net.ssl.SSLSocket;

import org.epics.pva.PVASettings;
import org.epics.pva.common.SecureSockets;
import org.epics.pva.common.SecureSockets.TLSHandshakeInfo;;

/** Listen to TCP connections
*
Expand Down Expand Up @@ -201,7 +204,7 @@ private void listen()
{ // Check TCP
final Socket client = tcp_server_socket.accept();
logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TCP client " + client.getRemoteSocketAddress());
new ServerTCPHandler(server, client);
new ServerTCPHandler(server, client, null);
}
catch (SocketTimeoutException timeout)
{ // Ignore
Expand All @@ -212,8 +215,15 @@ private void listen()
try
{ // Check TLS
final Socket client = tls_server_socket.accept();
logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TLS client " + client.getRemoteSocketAddress());
new ServerTCPHandler(server, client);
TLSHandshakeInfo tls_info = null;
if (client instanceof SSLSocket)
{
logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TLS client " + client.getRemoteSocketAddress());
tls_info = TLSHandshakeInfo.fromSocket((SSLSocket) client);
}
else
logger.log(Level.WARNING, () -> Thread.currentThread().getName() + " expected TLS client " + client.getRemoteSocketAddress() + " but did not get SSLSocket");
new ServerTCPHandler(server, client, tls_info);
}
catch (SocketTimeoutException timeout)
{ // Ignore
Expand Down

0 comments on commit 5d69683

Please sign in to comment.