From 21174667591d692d01a8f310a4ad51865463c5d9 Mon Sep 17 00:00:00 2001 From: "Endi S. Dewata" Date: Tue, 23 Jul 2024 17:46:33 -0500 Subject: [PATCH] Add NonBlockingSocketFactory The NonBlockingSocketFactory has been added to provide a non-blocking socket factory for PKIConnection. Eventually it will replace the DefaultSocketFactory once the support for OCSP and CRL has been added into JSSTrustManager. The test for HTTPS connector with NSS has been updated to use the non-blocking socket factory and validate the new error messages generated by JSSTrustManager. The test for HTTPS connector with PKCS #12 file will continue to use the blocking socket factory to prevent regressions. --- .github/workflows/server-https-nss-test.yml | 52 +++-- .../client/DefaultSocketFactory.java | 3 + .../client/NonBlockingSocketFactory.java | 189 ++++++++++++++++++ 3 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 base/common/src/main/java/org/dogtagpki/client/NonBlockingSocketFactory.java diff --git a/.github/workflows/server-https-nss-test.yml b/.github/workflows/server-https-nss-test.yml index 8b5aa18f1af..0d6a5692610 100644 --- a/.github/workflows/server-https-nss-test.yml +++ b/.github/workflows/server-https-nss-test.yml @@ -166,7 +166,10 @@ jobs: - name: Check PKI CLI with unknown issuer run: | # run PKI CLI but don't trust the cert - echo n | docker exec -i client pki -U https://pki.example.com:8443 info \ + echo n | docker exec -i client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://pki.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -179,10 +182,13 @@ jobs: # check stderr cat > expected << EOF WARNING: UNKNOWN_ISSUER encountered on 'CN=pki.example.com' indicates an unknown CA cert 'CN=CA Signing Certificate' - Trust this certificate (y/N)? SEVERE: FATAL: SSL alert sent: BAD_CERTIFICATE - IOException: Unable to write to socket: Failed to write to socket: (-5987) Invalid function argument. + Trust this certificate (y/N)? IOException: Unable to write to socket: Unable to validate CN=pki.example.com: Unknown issuer: CN=CA Signing Certificate EOF + # TODO: Update the expected stderr once the missing SSL alert is fixed + # Trust this certificate (y/N)? SEVERE: FATAL: SSL alert sent: UNKNOWN_CA + # IOException: Unable to write to socket: Unable to validate CN=pki.example.com: Unknown issuer: CN=CA Signing Certificate + diff expected stderr # the cert should not be stored @@ -193,7 +199,10 @@ jobs: - name: Check PKI CLI with unknown issuer with wrong hostname run: | # run PKI CLI with wrong hostname - echo n | docker exec -i client pki -U https://server.example.com:8443 info \ + echo n | docker exec -i client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://server.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -205,18 +214,24 @@ jobs: # check stderr cat > expected << EOF - WARNING: UNKNOWN_ISSUER encountered on 'CN=pki.example.com' indicates an unknown CA cert 'CN=CA Signing Certificate' WARNING: BAD_CERT_DOMAIN encountered on 'CN=pki.example.com' indicates a common-name mismatch - Trust this certificate (y/N)? SEVERE: FATAL: SSL alert sent: BAD_CERTIFICATE - IOException: Unable to write to socket: Failed to write to socket: (-12276) Unable to communicate securely with peer: requested domain name does not match the server's certificate. + WARNING: UNKNOWN_ISSUER encountered on 'CN=pki.example.com' indicates an unknown CA cert 'CN=CA Signing Certificate' + Trust this certificate (y/N)? IOException: Unable to write to socket: Unable to validate CN=pki.example.com: Bad certificate domain: CN=pki.example.com EOF + # TODO: Update the expected stderr once the missing SSL alert is fixed + # Trust this certificate (y/N)? SEVERE: FATAL: SSL alert sent: ACCESS_DENIED + # IOException: Unable to write to socket: Unable to validate CN=pki.example.com: Bad certificate domain: CN=pki.example.com + diff expected stderr - name: Check PKI CLI with newly trusted server cert run: | # run PKI CLI and trust the cert - echo y | docker exec -i client pki -U https://pki.example.com:8443 info \ + echo y | docker exec -i client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://pki.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -262,7 +277,10 @@ jobs: - name: Check PKI CLI with trusted server cert with wrong hostname run: | # run PKI CLI with wrong hostname - docker exec client pki -U https://server.example.com:8443 info \ + docker exec client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://server.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -283,7 +301,10 @@ jobs: - name: Check PKI CLI with already trusted server cert run: | # run PKI CLI with correct hostname - docker exec client pki -U https://pki.example.com:8443 info \ + docker exec client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://pki.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -301,7 +322,10 @@ jobs: run: | sleep 120 - docker exec client pki -U https://pki.example.com:8443 info \ + docker exec client pki \ + -D org.dogtagpki.client.socketFactory=org.dogtagpki.client.NonBlockingSocketFactory \ + -U https://pki.example.com:8443 \ + info \ > >(tee stdout) 2> >(tee stderr >&2) || true # check stdout @@ -314,10 +338,12 @@ jobs: # check stderr cat > expected << EOF ERROR: EXPIRED_CERTIFICATE encountered on 'CN=pki.example.com' results in a denied SSL server cert! - SEVERE: FATAL: SSL alert sent: BAD_CERTIFICATE - IOException: Unable to write to socket: Failed to write to socket: (-5987) Invalid function argument. + IOException: Unable to write to socket: Unable to validate CN=pki.example.com: Expired certificate: CN=pki.example.com EOF + # TODO: Update the expected stderr once the missing SSL alert is fixed + # SEVERE: FATAL: SSL alert sent: CERTIFICATE_EXPIRED + diff expected stderr - name: Stop PKI server diff --git a/base/common/src/main/java/org/dogtagpki/client/DefaultSocketFactory.java b/base/common/src/main/java/org/dogtagpki/client/DefaultSocketFactory.java index 3c444a0446a..a4115522269 100644 --- a/base/common/src/main/java/org/dogtagpki/client/DefaultSocketFactory.java +++ b/base/common/src/main/java/org/dogtagpki/client/DefaultSocketFactory.java @@ -22,6 +22,9 @@ import com.netscape.certsrv.client.PKIConnection; +/** + * This class provides blocking socket factory for PKIConnection. + */ public class DefaultSocketFactory implements SchemeLayeredSocketFactory { PKIConnection connection; diff --git a/base/common/src/main/java/org/dogtagpki/client/NonBlockingSocketFactory.java b/base/common/src/main/java/org/dogtagpki/client/NonBlockingSocketFactory.java new file mode 100644 index 00000000000..dd59a0b6f6e --- /dev/null +++ b/base/common/src/main/java/org/dogtagpki/client/NonBlockingSocketFactory.java @@ -0,0 +1,189 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.client; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Arrays; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +import org.apache.http.conn.scheme.SchemeLayeredSocketFactory; +import org.apache.http.params.HttpParams; +import org.mozilla.jss.CryptoManager; +import org.mozilla.jss.provider.javax.crypto.JSSTrustManager; +import org.mozilla.jss.ssl.SSLAlertDescription; +import org.mozilla.jss.ssl.SSLAlertEvent; +import org.mozilla.jss.ssl.SSLAlertLevel; +import org.mozilla.jss.ssl.SSLHandshakeCompletedEvent; +import org.mozilla.jss.ssl.SSLSocketListener; +import org.mozilla.jss.ssl.javax.JSSSocket; + +import com.netscape.certsrv.client.PKIConnection; + +/** + * This class provides non-blocking socket factory for PKIConnection. + */ +public class NonBlockingSocketFactory implements SchemeLayeredSocketFactory { + + public static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(NonBlockingSocketFactory.class); + + PKIConnection connection; + + public NonBlockingSocketFactory(PKIConnection connection) { + this.connection = connection; + } + + @Override + public Socket createSocket(HttpParams params) throws IOException { + return null; + } + + @Override + public Socket connectSocket(Socket socket, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + HttpParams params) + throws IOException, + UnknownHostException { + + String hostname = null; + int port = 0; + if (remoteAddress != null) { + hostname = remoteAddress.getHostName(); + port = remoteAddress.getPort(); + } + + int localPort = 0; + InetAddress localAddr = null; + + if (localAddress != null) { + localPort = localAddress.getPort(); + localAddr = localAddress.getAddress(); + } + + SSLSocketFactory socketFactory; + try { + CryptoManager.getInstance(); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("NssX509", "Mozilla-JSS"); + KeyManager[] kms = kmf.getKeyManagers(); + + // Create JSSTrustManager since the default JSSNativeTrustManager + // does not support hostname validation and cert approval callback. + // + // JSSTrustManager currently does not support cert validation with + // OCSP and CRL. + // + // TODO: Fix JSSTrustManager to support OCSP and CRL, then replace + // DefaultSocketFactory with this class. + + JSSTrustManager trustManager = new JSSTrustManager(); + trustManager.setHostname(hostname); + trustManager.setCallback(connection.getCallback()); + + TrustManager[] tms = new TrustManager[] { trustManager }; + + SSLContext ctx = SSLContext.getInstance("TLS", "Mozilla-JSS"); + ctx.init(kms, tms, null); + + socketFactory = ctx.getSocketFactory(); + + } catch (Exception e) { + throw new IOException("Unable to create SSL socket factory: " + e.getMessage(), e); + } + + JSSSocket jssSocket; + try { + if (socket == null) { + logger.info("Creating new SSL socket"); + jssSocket = (JSSSocket) socketFactory.createSocket( + InetAddress.getByName(hostname), + port, + localAddr, + localPort); + + } else { + logger.info("Creating SSL socket with existing socket"); + jssSocket = (JSSSocket) socketFactory.createSocket( + socket, + hostname, + port, + true); + } + + } catch (Exception e) { + throw new IOException("Unable to create SSL socket: " + e.getMessage(), e); + } + + jssSocket.setUseClientMode(true); + + String certNickname = connection.getConfig().getCertNickname(); + if (certNickname != null) { + logger.info("Client certificate: "+certNickname); + jssSocket.setCertFromAlias(certNickname); + } + + jssSocket.setListeners(Arrays.asList(new SSLSocketListener() { + + @Override + public void alertReceived(SSLAlertEvent event) { + + int intLevel = event.getLevel(); + SSLAlertLevel level = SSLAlertLevel.valueOf(intLevel); + + int intDescription = event.getDescription(); + SSLAlertDescription description = SSLAlertDescription.valueOf(intDescription); + + if (level == SSLAlertLevel.FATAL || logger.isInfoEnabled()) { + logger.error(level + ": SSL alert received: " + description); + } + } + + @Override + public void alertSent(SSLAlertEvent event) { + + int intLevel = event.getLevel(); + SSLAlertLevel level = SSLAlertLevel.valueOf(intLevel); + + int intDescription = event.getDescription(); + SSLAlertDescription description = SSLAlertDescription.valueOf(intDescription); + + if (level == SSLAlertLevel.FATAL || logger.isInfoEnabled()) { + logger.error(level + ": SSL alert sent: " + description); + } + } + + @Override + public void handshakeCompleted(SSLHandshakeCompletedEvent event) { + } + })); + + jssSocket.startHandshake(); + + return jssSocket; + } + + @Override + public boolean isSecure(Socket sock) { + // We only use this factory in the case of SSL Connections. + return true; + } + + @Override + public Socket createLayeredSocket(Socket socket, String target, int port, HttpParams params) + throws IOException, UnknownHostException { + // This method implementation is required to get SSL working. + return null; + } +}