diff --git a/core/pva/TLS.md b/core/pva/TLS.md index ffeecef53d..20ac1b1f77 100644 --- a/core/pva/TLS.md +++ b/core/pva/TLS.md @@ -34,7 +34,7 @@ This is an example recipe for getting started. 2) Start `pvacms -v`. It will create several files, including * `~/.config/pva/1.3/admin.p12`: Certificate for the `admin` user - + 3) For an IOC, request a hybrid server and client certificate. Note its "Certificate identifier": @@ -52,17 +52,17 @@ This is an example recipe for getting started. Approve ==> CERT:STATUS:e53ed409:15273288300286014953 ==> Completed Successfully ``` - * `~/.config/pva/1.3/server.p12`: Our server certificate (hybrid, for IOC) + * `~/.config/pva/1.3/server.p12`: Our server certificate (hybrid, for IOC) 4) Request a client certificate, note its identifier: ``` - $ authnstd + $ authnstd Keychain file created : /home/user/.config/pva/1.3/client.p12 Certificate identifier : e53ed409:11521018863975115478 ``` - Accept that certificate: + Accept that certificate: ``` $ EPICS_PVA_TLS_KEYCHAIN=~/.config/pva/1.3/admin.p12 \ @@ -94,13 +94,20 @@ To list certificate details: keytool -list -v -keystore ~/.config/pva/1.3/client.p12 -storepass "" ``` +Following the `pvacms` and `authnstd` messages, you will notice that secure PVA +maintains files in two locations. +If you need to start over, stop `pvacms`, delete those files, and then start `pvacms` again: + +``` +$ rm -rf ~/.config/pva ~/.local/share/pva +``` + For a test setup, all the above can be executed by a single user on one host. In a production setup, however, each human user should only have access to their own `client.p12` file. Pseudo-users running IOCs would have a `server.p12` file. Only an admin user on a designated host would have access to the remaining `pvacms` files, including the `admin.p12` file that permits accepting and revoking certificates. - Secure IOC ========== @@ -240,7 +247,7 @@ For lower level encryption information, add `-Djavax.net.debug=all`. For example, when receiving subscription updates for a string PV with status and timestamp, the log messages indicate that the encrypted data size of 74 bytes is almost twice the size -of the decrypted payload of 41 bytes: +of the decrypted payload of 41 bytes: ``` javax.net.ssl|DEBUG|91|TCP receiver /127.0.0.1|2023-05-05 15:57:37.299 EDT|SSLSocketInputRecord.java:214|READ: TLSv1.2 application_data, length = 74 @@ -400,24 +407,24 @@ In total, we now have the following: Keystore with public and private key of our Certification Authority (CA). This file needs to be guarded because it allows creating new IOC and client keystores. - + * `myca.cer`: Public certificate of the CA. Imported into any `*.p12` that needs to trust the CA. - + * `trust_ca.p12`: Truststore with public certificate of the CA. Equivalent to `myca.cer`, and some tools might directly use `myca.cer`, - but `trust_ca.p12` presents it in the commonly used PKCS12 `*.p12` file format. + but `trust_ca.p12` presents it in the commonly used PKCS12 `*.p12` file format. Clients can set their `EPICS_PVA_TLS_KEYCHAIN` to this file to communicate with IOCs, resulting in encryption and "ca" authentication. - + * `ioc.p12`: Keystore with public and private key of an IOC. The public key certificate is signed by the CA so that clients will trust it. To be used with `EPICS_PVAS_TLS_KEYCHAIN` of IOCs. - + * `myioc.cer`, `myioc.csr`: Public IOC certificate and certificate signing request. Intermediate files used to sign the IOC certificate. May be deleted. diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java index aa22e2451d..1f721b93a2 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.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 @@ -117,7 +117,7 @@ public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, this.guid = guid; // For TLS, check if the socket has a name that's used to authenticate - x509_name = tls ? SecureSockets.getLocalPrincipalName((SSLSocket) socket) : null; + x509_name = tls ? SecureSockets.getPrincipalCN(((SSLSocket) socket).getSession().getLocalPrincipal()) : null; // For default EPICS_CA_CONN_TMO: 30 sec, send echo at ~15 sec: // Check every ~3 seconds 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 65959f2667..31b88c3e26 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Oak Ridge National Laboratory. + * Copyright (c) 2023-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 @@ -14,6 +14,9 @@ import java.net.ServerSocket; import java.net.Socket; import java.security.KeyStore; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.logging.Level; import javax.naming.ldap.LdapName; @@ -22,6 +25,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; @@ -175,19 +179,84 @@ public static Socket createClientSocket(final InetSocketAddress address, final b socket.setEnabledProtocols(PROTOCOLS); // Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket socket.startHandshake(); + + if (logger.isLoggable(Level.FINE)) + { + try + { // Key sections of example server certificate chain: + // + // Certificate[1]: + // Owner: O=xxx.site.org, C=US, CN=ioc + // Issuer: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA + // + // #1: ObjectId: 1.3.6.1.4.1.37427.1 Criticality=false + // 0000: 43 45 52 54 3A 53 54 41 54 55 53 3A 64 30 62 62 CERT:STATUS:d0bb... + // + // Certificate[2]: + // Owner: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA + // Issuer: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA + // + // #1: ObjectId: 1.3.6.1.4.1.37427.1 Criticality=false + // 0000: 43 45 52 54 3A 53 54 41 54 55 53 3A 64 30 62 62 CERT:STATUS:d0bb... + final SSLSession session = socket.getSession(); + logger.log(Level.FINE, "Server name: '" + getPrincipalCN(session.getPeerPrincipal()) + "'"); + for (Certificate cert : session.getPeerCertificates()) + if (cert instanceof X509Certificate x509) + { + logger.log(Level.FINE, "* " + x509.getSubjectX500Principal()); + if (session.getPeerPrincipal().equals(x509.getSubjectX500Principal())) + logger.log(Level.FINE, " - Server/IOC CN"); + if (x509.getBasicConstraints() >= 0) + logger.log(Level.FINE, " - Certificate Authority"); + logger.log(Level.FINE, " - Expires " + x509.getNotAfter()); + if (x509.getSubjectX500Principal().equals(x509.getIssuerX500Principal())) + logger.log(Level.FINE, " - Self-signed"); + + byte[] value = x509.getExtensionValue("1.3.6.1.4.1.37427.1"); + logger.log(Level.FINE, " - Status PV: " + decodeDERString(value)); + } + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Error while logging result of handshake with server", ex); + } + } + return socket; } - /** Get name from local principal + /** Decode DER String + * @param der_value + * @return + * @throws Exception on error + */ + public static String decodeDERString(final byte[] der_value) throws Exception + { + if (der_value == null) + return null; + // https://en.wikipedia.org/wiki/X.690#DER_encoding: + // Type 4, length 0..127, characters + if (der_value.length < 2) + throw new Exception("Need DER type and size, only received " + der_value.length + " bytes"); + if (der_value[0] != 0x04) + throw new Exception(String.format("Expected DER Octet String 0x04, got 0x%02X", der_value[0])); + if (der_value[1] < 0) + throw new Exception("Can only handle strings of length 0-127, got " + der_value[1]); + if (der_value[1] != der_value.length-2) + throw new Exception("DER string length " + der_value[1] + " but " + (der_value.length-2) + " data items"); + return new String(der_value, 2, der_value[1]); + } + + /** Get CN from principal * - * @param socket {@link SSLSocket} that may have local principal - * @return Name (without "CN=..") if socket has certificate to authenticate or null + * @param principal {@link Principal} that may have a CN + * @return CN value (without "CN=..") or null */ - public static String getLocalPrincipalName(final SSLSocket socket) + public static String getPrincipalCN(final Principal principal) { try { - final LdapName ldn = new LdapName(socket.getSession().getLocalPrincipal().getName()); + final LdapName ldn = new LdapName(principal.getName()); for (Rdn rdn : ldn.getRdns()) if (rdn.getType().equals("CN")) return (String) rdn.getValue(); @@ -229,14 +298,16 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti { // 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 Principal principal = socket.getSession().getPeerPrincipal(); + String name = getPrincipalCN(principal); + if (name == null) + { + logger.log(Level.WARNING, "Peer " + socket.getInetAddress() + " sent '" + principal + "' as principal name, expected 'CN=...'"); + name = principal.getName(); + } + final TLSHandshakeInfo info = new TLSHandshakeInfo(); info.name = name; - info.hostname = socket.getInetAddress().getHostName(); return info;