Skip to content

Commit

Permalink
Merge pull request #2811 from ControlSystemStudio/tls_options
Browse files Browse the repository at this point in the history
 PVA: Support EPICS_PVAS_TLS_OPTIONS
  • Loading branch information
kasemir authored Sep 7, 2023
2 parents c7488bb + 674dbfc commit 97a78cb
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 13 deletions.
23 changes: 19 additions & 4 deletions core/pva/TLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ they are compatible with generic open SSL tools:
openssl pkcs12 -info -in KEYSTORE -nodes
```

The essential commands are also in `make_tls_simple.sh`


Step 3: Configure and run the demo server
-------

Set environment variables to inform the server about its keystore.
Set environment variable `EPICS_PVAS_TLS_KEYCHAIN` to inform the server about its keystore.
The format of this setting is `/path/to/file;password`.
Then run a demo server:
If you used `make_tls_simple.sh`, that would be `demo/KEYSTORE;changeit`.

Then run a demo server:

```
export EPICS_PVAS_TLS_KEYCHAIN="/path/to/KEYSTORE;changeit"
Expand All @@ -78,7 +81,8 @@ java -cp target/classes org.epics.pva.server.ServerDemo
Step 4: Configure and run the demo client
-------

Set environment variables to inform the client about its truststore.
Set environment variable `EPICS_PVA_TLS_KEYCHAIN` to inform the client about its truststore.
If you used `make_tls_simple.sh`, that would be `demo/TRUSTSTORE;changeit`.
Then run a demo client (`-v 5` to see protocol detail):

```
Expand All @@ -87,6 +91,7 @@ java -cp target/classes org.epics.pva.client.PVAClientMain get -v 5 demo
# Or: ./pvaclient get -v 5 demo
```

On both the server and the client note how they mention "TLS" in their log messages.

Logging
-------
Expand Down Expand Up @@ -192,6 +197,7 @@ keytool -list -v -keystore ioc.p12 -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`.

See `make_tls_ca.sh` for a copy of the essential commands.
You can create additional server files `ioc1.p12`, `ioc2.p12` and have each IOC use its own key pair.
Clients will trust them without any changes to the `trust_ca.p12`
as long as the IOC certificates are signed by your CA.
Expand Down Expand Up @@ -237,6 +243,12 @@ We can now run the server with `EPICS_PVAS_TLS_KEYCHAIN=/path/to/ioc.p12;changei
`EPICS_PVA_TLS_KEYCHAIN=/path/to/client.p12;changeit`.
The server will identify the client as "Fred F.".

By default, the server supports clients with certificate and x509 authentication,
but client certificates are not required.
By setting `EPICS_PVAS_TLS_OPTIONS="client_cert=require"`, the server will
abort the initial TLS handshake for clients that do not have a certificate.


In total, we now have the following:

* `KEYSTORE`, `TRUSTSTORE`:
Expand Down Expand Up @@ -276,7 +288,10 @@ In total, we now have the following:
providing a client name for x509 authentication.
Clients can set their `EPICS_PVA_TLS_KEYCHAIN` to this file to
communicate with IOCs using x509 authentication.

If the server runs with `EPICS_PVAS_TLS_OPTIONS="client_cert=require"`,
the clients MUST use a client certificate like `client.p12`.
Clients with just `trust_ca.p12` will fail during the initial TLS handshake.

* `myclient.cer`, `myclient.csr`:
Intermediate files used to sign the client certificate.
May be deleted.
Expand Down
10 changes: 7 additions & 3 deletions core/pva/make_tls_ca → core/pva/make_tls_ca.sh
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
# More elaborate example, see TLS.md "Use a Certification Authority"
rm -rf demo
mkdir demo
cd demo

# Create our own CA, and make its public certificate available as `myca.cer`:
keytool -genkeypair -alias myca -keystore ca.p12 -storepass changeit -dname "CN=myca" -keyalg RSA -ext BasicConstraints=ca:true
keytool -list -v -keystore ca.p12 -storepass changeit
keytool -exportcert -alias myca -keystore ca.p12 -storepass changeit -rfc -file myca.cer
keytool -printcert -file myca.cer

# For clients, create a truststore that holds the public certificate of our CA
keytool -importcert -alias myca -keystore trust_ca.p12 -storepass changeit -file myca.cer -noprompt
keytool -list -v -keystore trust_ca.p12 -storepass changeit

# Now create a server keypair for use by the IOC:
keytool -genkeypair -alias myioc -keystore ioc.p12 -storepass changeit -dname "CN=myioc" -keyalg RSA
keytool -list -v -keystore ioc.p12 -storepass changeit

# Create signing request, sign with our CA, import signed cert into ioc.p12
keytool -certreq -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.csr
keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:myioc -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myioc.csr -outfile myioc.cer
keytool -printcert -file myioc.cer

keytool -importcert -alias myca -keystore ioc.p12 -storepass changeit -file myca.cer -noprompt
keytool -importcert -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.cer
keytool -list -v -keystore ioc.p12 -storepass changeit

# Create client keypair so client can authenticate as "Fred F."
keytool -genkeypair -alias myclient -keystore client.p12 -storepass changeit -dname "CN=Fred F." -keyalg RSA
keytool -list -v -keystore client.p12 -storepass changeit

# Sign client certificate with our CA
keytool -certreq -alias myclient -keystore client.p12 -storepass changeit -file myclient.csr
keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:client -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myclient.csr -outfile myclient.cer
keytool -printcert -file myclient.cer

keytool -importcert -alias myca -keystore client.p12 -storepass changeit -file myca.cer -noprompt
keytool -importcert -alias myclient -keystore client.p12 -storepass changeit -file myclient.cer
keytool -list -v -keystore client.p12 -storepass changeit


echo "*************************************************************"
echo "***************** trust_ca **********************************"
echo "*************************************************************"
Expand Down
7 changes: 7 additions & 0 deletions core/pva/make_tls_simple → core/pva/make_tls_simple.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# Minimal example, see TLS.md steps 1-4

rm -rf demo
mkdir demo
cd demo


# Creates a key pair for the server
keytool -genkey -alias mykey -dname "CN=server" -keystore KEYSTORE -storepass changeit -keyalg RSA
# Export the public key
keytool -export -alias mykey -keystore KEYSTORE -storepass changeit -rfc -file mykey.cer

# Create a trust store for the client to make it aware of the server's public key
keytool -import -alias mykey -file mykey.cer -keystore TRUSTSTORE -storepass changeit -noprompt

echo "*************************************************************"
Expand Down
20 changes: 20 additions & 0 deletions core/pva/src/main/java/org/epics/pva/PVASettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ public class PVASettings
*/
public static String EPICS_PVAS_TLS_KEYCHAIN = "";

/** Secure server options
*
* <ul>
* <li><code>client_cert=optional</code>:
* Default; clients can provide certificate for "X509" authentication,
* but may also use "ca" or "anonymous" authentication
* <li><code>client_cert=require</code>:
* Clients must provide certificate for "X509" authentication.
* Socket with otherwise be closed during initial handshake.
* Server will log "SSLHandshakeException: Empty client certificate chain",
* client will log "SSLHandshakeException: Received fatal alert: bad_certificate"
* </ul>
*/
public static String EPICS_PVAS_TLS_OPTIONS = "";

/** Does EPICS_PVAS_TLS_OPTIONS contain "client_cert=require"? */
public static boolean require_client_cert;

/** 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.
Expand Down Expand Up @@ -237,6 +255,8 @@ public class PVASettings
EPICS_PVA_CONN_TMO = get("EPICS_PVA_CONN_TMO", EPICS_PVA_CONN_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_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_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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019-2022 Oak Ridge National Laboratory.
* Copyright (c) 2019-2023 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
Expand All @@ -16,6 +16,7 @@
import java.util.logging.Handler;
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.PVAData;
Expand Down Expand Up @@ -59,6 +60,7 @@ private static void help()
private static void setLogLevel(final Level level)
{
PVASettings.logger.setLevel(level);
Logger.getLogger("jdk.event.security").setLevel(level);
}

/** Get info for each PV on the list, then close PV
Expand Down
21 changes: 17 additions & 4 deletions core/pva/src/main/java/org/epics/pva/common/SecureSockets.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private static SSLContext createContext(final String keychain_setting) throws Ex
pass = "".toCharArray();
}

logger.log(Level.INFO, () -> "Loading keychain '" + path + "'");
logger.log(Level.CONFIG, () -> "Loading keychain '" + path + "'");

final KeyStore key_store = KeyStore.getInstance("PKCS12");
key_store.load(new FileInputStream(path), pass);
Expand Down Expand Up @@ -119,10 +119,19 @@ 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();
final SSLServerSocket ssl = (SSLServerSocket) socket;

// Request, but don't require, client's certificate with 'principal' name for x509 authentication
((SSLServerSocket) socket).setWantClientAuth(true);
((SSLServerSocket) socket).setEnabledProtocols(PROTOCOLS);
// Do we require client's certificate with 'principal' name for x509 authentication,
// and initial handshake will otherwise fail?
// Or do we support a client certificate, but it's not essential?
if (PVASettings.require_client_cert)
{
ssl.setNeedClientAuth(true);
logger.log(Level.FINE, "Server requires client certificate");
}
else
ssl.setWantClientAuth(true);
ssl.setEnabledProtocols(PROTOCOLS);
}
else
socket = new ServerSocket();
Expand Down Expand Up @@ -204,6 +213,10 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti
// "This method is synchronous for the initial handshake on a connection
// and returns when the negotiated handshake is complete",
// so no need to addHandshakeCompletedListener()

// If server socket was configured to require client authentication,
// there will be an SSLHandshakeException with message "Empty client certificate chain",
// but no obvious way to catch that
socket.startHandshake();

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;

import org.epics.pva.PVASettings;
Expand Down Expand Up @@ -219,7 +220,15 @@ private void listen()
if (client instanceof SSLSocket)
{
logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TLS client " + client.getRemoteSocketAddress());
tls_info = TLSHandshakeInfo.fromSocket((SSLSocket) client);
try
{
tls_info = TLSHandshakeInfo.fromSocket((SSLSocket) client);
}
catch (SSLHandshakeException ssl)
{
logger.log(Level.FINE, "SSL Handshake error for " + client.getRemoteSocketAddress(), ssl);
continue;
}
}
else
logger.log(Level.WARNING, () -> Thread.currentThread().getName() + " expected TLS client " + client.getRemoteSocketAddress() + " but did not get SSLSocket");
Expand Down

0 comments on commit 97a78cb

Please sign in to comment.