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; + } +}