From 67ad8d69b7b045d053c16c11db225c472f77dff5 Mon Sep 17 00:00:00 2001 From: kasemir Date: Thu, 7 Sep 2023 11:03:48 -0400 Subject: [PATCH 1/2] PVA: Support EPICS_PVAS_TLS_OPTIONS="client_cert=optional" .. to track corresponding support in PVXS --- core/pva/TLS.md | 23 +++++++++++++++---- core/pva/{make_tls_ca => make_tls_ca.sh} | 10 +++++--- .../{make_tls_simple => make_tls_simple.sh} | 7 ++++++ .../main/java/org/epics/pva/PVASettings.java | 20 ++++++++++++++++ .../org/epics/pva/client/PVAClientMain.java | 4 +++- .../org/epics/pva/common/SecureSockets.java | 21 +++++++++++++---- .../epics/pva/server/ServerTCPListener.java | 11 ++++++++- 7 files changed, 83 insertions(+), 13 deletions(-) rename core/pva/{make_tls_ca => make_tls_ca.sh} (86%) rename core/pva/{make_tls_simple => make_tls_simple.sh} (82%) diff --git a/core/pva/TLS.md b/core/pva/TLS.md index 3be3b0be21..0befc5db0d 100644 --- a/core/pva/TLS.md +++ b/core/pva/TLS.md @@ -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" @@ -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): ``` @@ -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 ------- @@ -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. @@ -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`: @@ -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. diff --git a/core/pva/make_tls_ca b/core/pva/make_tls_ca.sh similarity index 86% rename from core/pva/make_tls_ca rename to core/pva/make_tls_ca.sh index a616d83584..18a61aa070 100755 --- a/core/pva/make_tls_ca +++ b/core/pva/make_tls_ca.sh @@ -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 "*************************************************************" diff --git a/core/pva/make_tls_simple b/core/pva/make_tls_simple.sh similarity index 82% rename from core/pva/make_tls_simple rename to core/pva/make_tls_simple.sh index 70beb3d8a4..29926cbf9d 100755 --- a/core/pva/make_tls_simple +++ b/core/pva/make_tls_simple.sh @@ -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 "*************************************************************" 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 da6666b90c..0be6ad82bd 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -144,6 +144,24 @@ public class PVASettings */ public static String EPICS_PVAS_TLS_KEYCHAIN = ""; + /** Secure server options + * + * + */ + 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. @@ -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); 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 d70082b7d3..93d9b14f7a 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 @@ -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 @@ -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; @@ -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 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 d74f46828e..9710eb952b 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 @@ -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); @@ -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(); @@ -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 diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java index b3cdb792b7..aeee63d1ff 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java @@ -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; @@ -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"); From 674dbfcaf608f0dfa1a00480eb4d763e3569344e Mon Sep 17 00:00:00 2001 From: kasemir Date: Thu, 7 Sep 2023 11:24:50 -0400 Subject: [PATCH 2/2] typo --- core/pva/TLS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pva/TLS.md b/core/pva/TLS.md index 0befc5db0d..7129a59cd6 100644 --- a/core/pva/TLS.md +++ b/core/pva/TLS.md @@ -66,7 +66,7 @@ The essential commands are also in `make_tls_simple.sh` Step 3: Configure and run the demo server ------- -Set environment variable `EPICS_PVAS_TLS_KEYCHAIN`` 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`. If you used `make_tls_simple.sh`, that would be `demo/KEYSTORE;changeit`.