Skip to content

Commit bb05d99

Browse files
author
kasemir
committed
PVA: Client logs server info at FINE level
1 parent fe56add commit bb05d99

File tree

3 files changed

+103
-25
lines changed

3 files changed

+103
-25
lines changed

core/pva/TLS.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ This is an example recipe for getting started.
3434
2) Start `pvacms -v`. It will create several files, including
3535

3636
* `~/.config/pva/1.3/admin.p12`: Certificate for the `admin` user
37-
37+
3838
3) For an IOC, request a hybrid server and client certificate.
3939
Note its "Certificate identifier":
4040

@@ -52,17 +52,17 @@ This is an example recipe for getting started.
5252
Approve ==> CERT:STATUS:e53ed409:15273288300286014953 ==> Completed Successfully
5353
```
5454

55-
* `~/.config/pva/1.3/server.p12`: Our server certificate (hybrid, for IOC)
55+
* `~/.config/pva/1.3/server.p12`: Our server certificate (hybrid, for IOC)
5656

5757
4) Request a client certificate, note its identifier:
5858

5959
```
60-
$ authnstd
60+
$ authnstd
6161
Keychain file created : /home/user/.config/pva/1.3/client.p12
6262
Certificate identifier : e53ed409:11521018863975115478
6363
```
6464

65-
Accept that certificate:
65+
Accept that certificate:
6666

6767
```
6868
$ EPICS_PVA_TLS_KEYCHAIN=~/.config/pva/1.3/admin.p12 \
@@ -94,13 +94,20 @@ To list certificate details:
9494
keytool -list -v -keystore ~/.config/pva/1.3/client.p12 -storepass ""
9595
```
9696

97+
Following the `pvacms` and `authnstd` messages, you will notice that secure PVA
98+
maintains files in two locations.
99+
If you need to start over, stop `pvacms`, delete those files, and then start `pvacms` again:
100+
101+
```
102+
$ rm -rf ~/.config/pva ~/.local/share/pva
103+
```
104+
97105
For a test setup, all the above can be executed by a single user on one host.
98106
In a production setup, however, each human user should only have access to their own `client.p12` file.
99107
Pseudo-users running IOCs would have a `server.p12` file.
100108
Only an admin user on a designated host would have access to the remaining `pvacms` files,
101109
including the `admin.p12` file that permits accepting and revoking certificates.
102110

103-
104111
Secure IOC
105112
==========
106113

@@ -240,7 +247,7 @@ For lower level encryption information, add `-Djavax.net.debug=all`.
240247

241248
For example, when receiving subscription updates for a string PV with status and timestamp,
242249
the log messages indicate that the encrypted data size of 74 bytes is almost twice the size
243-
of the decrypted payload of 41 bytes:
250+
of the decrypted payload of 41 bytes:
244251

245252
```
246253
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:
400407
Keystore with public and private key of our Certification Authority (CA).
401408
This file needs to be guarded because it allows creating new IOC
402409
and client keystores.
403-
410+
404411
* `myca.cer`:
405412
Public certificate of the CA.
406413
Imported into any `*.p12` that needs to trust the CA.
407-
414+
408415
* `trust_ca.p12`:
409416
Truststore with public certificate of the CA.
410417
Equivalent to `myca.cer`, and some tools might directly use `myca.cer`,
411-
but `trust_ca.p12` presents it in the commonly used PKCS12 `*.p12` file format.
418+
but `trust_ca.p12` presents it in the commonly used PKCS12 `*.p12` file format.
412419
Clients can set their `EPICS_PVA_TLS_KEYCHAIN` to this file to
413420
communicate with IOCs, resulting in encryption and "ca" authentication.
414-
421+
415422
* `ioc.p12`:
416423
Keystore with public and private key of an IOC.
417424
The public key certificate is signed by the CA so that clients
418425
will trust it.
419426
To be used with `EPICS_PVAS_TLS_KEYCHAIN` of IOCs.
420-
427+
421428
* `myioc.cer`, `myioc.csr`: Public IOC certificate and certificate signing request.
422429
Intermediate files used to sign the IOC certificate.
423430
May be deleted.

core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019-2023 Oak Ridge National Laboratory.
2+
* Copyright (c) 2019-2025 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -117,7 +117,7 @@ public ClientTCPHandler(final PVAClient client, final InetSocketAddress address,
117117
this.guid = guid;
118118

119119
// For TLS, check if the socket has a name that's used to authenticate
120-
x509_name = tls ? SecureSockets.getLocalPrincipalName((SSLSocket) socket) : null;
120+
x509_name = tls ? SecureSockets.getPrincipalCN(((SSLSocket) socket).getSession().getLocalPrincipal()) : null;
121121

122122
// For default EPICS_CA_CONN_TMO: 30 sec, send echo at ~15 sec:
123123
// Check every ~3 seconds

core/pva/src/main/java/org/epics/pva/common/SecureSockets.java

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023 Oak Ridge National Laboratory.
2+
* Copyright (c) 2023-2025 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -14,6 +14,9 @@
1414
import java.net.ServerSocket;
1515
import java.net.Socket;
1616
import java.security.KeyStore;
17+
import java.security.Principal;
18+
import java.security.cert.Certificate;
19+
import java.security.cert.X509Certificate;
1720
import java.util.logging.Level;
1821

1922
import javax.naming.ldap.LdapName;
@@ -22,6 +25,7 @@
2225
import javax.net.ssl.SSLContext;
2326
import javax.net.ssl.SSLServerSocket;
2427
import javax.net.ssl.SSLServerSocketFactory;
28+
import javax.net.ssl.SSLSession;
2529
import javax.net.ssl.SSLSocket;
2630
import javax.net.ssl.SSLSocketFactory;
2731
import javax.net.ssl.TrustManagerFactory;
@@ -175,19 +179,84 @@ public static Socket createClientSocket(final InetSocketAddress address, final b
175179
socket.setEnabledProtocols(PROTOCOLS);
176180
// Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket
177181
socket.startHandshake();
182+
183+
if (logger.isLoggable(Level.FINE))
184+
{
185+
try
186+
{ // Key sections of example server certificate chain:
187+
//
188+
// Certificate[1]:
189+
// Owner: O=xxx.site.org, C=US, CN=ioc
190+
// Issuer: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA
191+
//
192+
// #1: ObjectId: 1.3.6.1.4.1.37427.1 Criticality=false
193+
// 0000: 43 45 52 54 3A 53 54 41 54 55 53 3A 64 30 62 62 CERT:STATUS:d0bb...
194+
//
195+
// Certificate[2]:
196+
// Owner: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA
197+
// Issuer: OU=EPICS Certificate Authority, O=ca.epics.org, C=US, CN=EPICS Root CA
198+
//
199+
// #1: ObjectId: 1.3.6.1.4.1.37427.1 Criticality=false
200+
// 0000: 43 45 52 54 3A 53 54 41 54 55 53 3A 64 30 62 62 CERT:STATUS:d0bb...
201+
final SSLSession session = socket.getSession();
202+
logger.log(Level.FINE, "Server name: '" + getPrincipalCN(session.getPeerPrincipal()) + "'");
203+
for (Certificate cert : session.getPeerCertificates())
204+
if (cert instanceof X509Certificate x509)
205+
{
206+
logger.log(Level.FINE, "* " + x509.getSubjectX500Principal());
207+
if (session.getPeerPrincipal().equals(x509.getSubjectX500Principal()))
208+
logger.log(Level.FINE, " - Server/IOC CN");
209+
if (x509.getBasicConstraints() >= 0)
210+
logger.log(Level.FINE, " - Certificate Authority");
211+
logger.log(Level.FINE, " - Expires " + x509.getNotAfter());
212+
if (x509.getSubjectX500Principal().equals(x509.getIssuerX500Principal()))
213+
logger.log(Level.FINE, " - Self-signed");
214+
215+
byte[] value = x509.getExtensionValue("1.3.6.1.4.1.37427.1");
216+
logger.log(Level.FINE, " - Status PV: " + decodeDERString(value));
217+
}
218+
}
219+
catch (Exception ex)
220+
{
221+
logger.log(Level.WARNING, "Error while logging result of handshake with server", ex);
222+
}
223+
}
224+
178225
return socket;
179226
}
180227

181-
/** Get name from local principal
228+
/** Decode DER String
229+
* @param der_value
230+
* @return
231+
* @throws Exception on error
232+
*/
233+
public static String decodeDERString(final byte[] der_value) throws Exception
234+
{
235+
if (der_value == null)
236+
return null;
237+
// https://en.wikipedia.org/wiki/X.690#DER_encoding:
238+
// Type 4, length 0..127, characters
239+
if (der_value.length < 2)
240+
throw new Exception("Need DER type and size, only received " + der_value.length + " bytes");
241+
if (der_value[0] != 0x04)
242+
throw new Exception(String.format("Expected DER Octet String 0x04, got 0x%02X", der_value[0]));
243+
if (der_value[1] < 0)
244+
throw new Exception("Can only handle strings of length 0-127, got " + der_value[1]);
245+
if (der_value[1] != der_value.length-2)
246+
throw new Exception("DER string length " + der_value[1] + " but " + (der_value.length-2) + " data items");
247+
return new String(der_value, 2, der_value[1]);
248+
}
249+
250+
/** Get CN from principal
182251
*
183-
* @param socket {@link SSLSocket} that may have local principal
184-
* @return Name (without "CN=..") if socket has certificate to authenticate or <code>null</code>
252+
* @param principal {@link Principal} that may have a CN
253+
* @return CN value (without "CN=..") or <code>null</code>
185254
*/
186-
public static String getLocalPrincipalName(final SSLSocket socket)
255+
public static String getPrincipalCN(final Principal principal)
187256
{
188257
try
189258
{
190-
final LdapName ldn = new LdapName(socket.getSession().getLocalPrincipal().getName());
259+
final LdapName ldn = new LdapName(principal.getName());
191260
for (Rdn rdn : ldn.getRdns())
192261
if (rdn.getType().equals("CN"))
193262
return (String) rdn.getValue();
@@ -229,14 +298,16 @@ public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Excepti
229298
{
230299
// No way to check if there is peer info (certificates, principal, ...)
231300
// other then success vs. exception..
232-
String name = socket.getSession().getPeerPrincipal().getName();
233-
if (name.startsWith("CN="))
234-
name = name.substring(3);
235-
else
236-
logger.log(Level.WARNING, "Peer " + socket.getInetAddress() + " sent '" + name + "' as principal name, expected 'CN=...'");
301+
final Principal principal = socket.getSession().getPeerPrincipal();
302+
String name = getPrincipalCN(principal);
303+
if (name == null)
304+
{
305+
logger.log(Level.WARNING, "Peer " + socket.getInetAddress() + " sent '" + principal + "' as principal name, expected 'CN=...'");
306+
name = principal.getName();
307+
}
308+
237309
final TLSHandshakeInfo info = new TLSHandshakeInfo();
238310
info.name = name;
239-
240311
info.hostname = socket.getInetAddress().getHostName();
241312

242313
return info;

0 commit comments

Comments
 (0)