diff --git a/RELEASE.md b/RELEASE.md index 70cb1c9..62e1fb1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ Release Notes for P4Java, the Perforce Java API - Version 2021.2 Patch 3 + Version 2021.2 Patch 5 Introduction @@ -120,6 +120,42 @@ Known Limitations /lib/security/local_policy.jar /lib/security/US_export_policy.jar + +------------------------------------------- +Updates in 2021.2 Patch 5 + + #2299942 (Job #108736) + P4TRUST is no longer required for SSL connections where the server + provides a certificate that's not self-signed and the certificate + chain can be verified by the client. If verified, P4TRUST is + not required. + + The default java truststore is used unless you specify an + alternative truststore with java system properties + javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword + + Chain Validation can be disabled using p4java property + secureClientCertValidate set to 0 which does P4TRUST only. + Setting to 2 will skip Chain validation and will ensure + the server certificates' subject or subject alternate names + match the hostname in the server URI. The default of 1 will + validate the chain. Both 1 and 2 fallback to P4TRUST if + the chain cannot be validated. + + Fingerprints will now read and write the hostname in + addition to the IP in the P4TRUST file. Set the p4java property + secureClientTrustName to 0 to only write the IP. The default of + 1 writes entries for both the IP and hostname. A value of 2 + will only write the hostname. A matching fingerprint for either + the IP or hostname will establish trust. + + +------------------------------------------- +Updates in 2021.2 Patch 4 + + #2286431 (Job #099302) + Fixed parallel sync authetication issue on case insensitive servers. + Fixes JENKINS-48525 and JENKINS-68104. ------------------------------------------- Updates in 2021.2 Patch 3 @@ -128,7 +164,7 @@ Updates in 2021.2 Patch 3 Fixed parallel sync batchsize. #2277668 (Job #110201) - Parallel sync now passes charset to parallel threads. + Parallel sync now passes charset to parallel threads. ------------------------------------------- Updates in 2021.2 Patch 2 diff --git a/src/main/java/com/perforce/p4java/impl/generic/core/DefaultParallelSync.java b/src/main/java/com/perforce/p4java/impl/generic/core/DefaultParallelSync.java index f08477f..c6a91e6 100644 --- a/src/main/java/com/perforce/p4java/impl/generic/core/DefaultParallelSync.java +++ b/src/main/java/com/perforce/p4java/impl/generic/core/DefaultParallelSync.java @@ -75,13 +75,14 @@ public void run() { server.setCurrentServerInfo(cmdEnv.getServer().getCurrentServerInfo()); server.setUserName(cmdEnv.getServer().getUserName()); - server.setAuthTicket(cmdEnv.getServer().getAuthTicket()); server.setCurrentClient(cmdEnv.getServer().getCurrentClient()); server.setWorkingDirectory(cmdEnv.getServer().getWorkingDirectory()); server.setTrustFilePath(cmdEnv.getServer().getTrustFilePath()); server.setTicketsFilePath(cmdEnv.getServer().getTicketsFilePath()); server.setCharsetName(cmdEnv.getServer().getCharsetName()); server.connect(); + // P4JAVA-1264: must call setAuthTicket() after connect() to properly cache the ticket. + server.setAuthTicket(cmdEnv.getServer().getAuthTicket()); //pass the result to the handle result Map[] results = server.execMapCmd("transmit", args.toArray(new String[]{}), null); diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/NtsServerImpl.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/NtsServerImpl.java index d47127a..d112163 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/NtsServerImpl.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/NtsServerImpl.java @@ -1,5 +1,5 @@ -/** - * +/* + * Copyright 2009 - 2022 Perforce Software Inc., All Rights Reserved. */ package com.perforce.p4java.impl.mapbased.rpc; @@ -614,8 +614,9 @@ protected ExternalEnv setupCmd(String cmdName, String[] cmdArgs, // Should use tags? boolean useTags = useTags(cmdName, cmdArgs, inMap, isStream); - // Check fingerprint - checkFingerprint(rpcConnection); + // Check certificate chain and/or fingerprint. + // An exception (ConnectionException) is thrown if ssl but not trusted. + trustConnectionCheck(rpcConnection); ExternalEnv env = new ExternalEnv( this.getUsageOptions().getProgramName(), @@ -749,4 +750,5 @@ public IServerAddress getServerAddressDetails() { return builder.build(); } + } diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/OneShotServerImpl.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/OneShotServerImpl.java index 47be4f1..a1b8a54 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/OneShotServerImpl.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/OneShotServerImpl.java @@ -1,5 +1,5 @@ -/** - * +/* + * Copyright 2009 - 2022 Perforce Software Inc., All Rights Reserved. */ package com.perforce.p4java.impl.mapbased.rpc; @@ -604,9 +604,10 @@ protected ExternalEnv setupCmd(RpcPacketDispatcher dispatcher, // Should use tags? boolean useTags = useTags(cmdName, cmdArgs, inMap, isStream); - - // Check fingerprint - checkFingerprint(rpcConnection); + + // Check certificate chain and/or fingerprint. + // An exception (ConnectionException) is thrown if ssl but not trusted. + trustConnectionCheck(rpcConnection); ExternalEnv env = new ExternalEnv( this.getUsageOptions().getProgramName(), @@ -694,6 +695,7 @@ protected ExternalEnv setupCmd(RpcPacketDispatcher dispatcher, return env; } + /** * Get server address object * diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcPropertyDefs.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcPropertyDefs.java index cac6eb7..235ffed 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcPropertyDefs.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcPropertyDefs.java @@ -200,6 +200,38 @@ public class RpcPropertyDefs { */ public static final String RPC_DEFAULT_SECURE_SOCKET_PROTOCOL = "TLS"; + /** + * Client Certificate validation Method, corresponds to p4api ssl.client.cert.validate + *

+ * 0: always use the P4TRUST mechanism. This is pre 2022.1 behavior.
+ * 1: validate the certificate chain (default)
+ * 2: validate the subject matches the P4PORT. The chain is not validated. but the CN of the + * certificate is compared to the host in the P4PORT.
+ */ + public static final String RPC_SECURE_CLIENT_CERT_VALIDATE_NICK = "secureClientCertValidate"; + + + /** + * Default for Certificate validation Method + */ + public static final int RPC_DEFAULT_SECURE_CLIENT_CERT_VALIDATE = 1; + + /** + * P4TRUST file entries, corresponds to p4api ssl.client.trust.name + *
+ * 0: Only IP address This is pre 2022.1 behavior.
+ * 1: both IP and hostname (default)
+ * 2: Only hostname The chain is not validated. but the CN of the + * certificate is compared to the host in the P4PORT.
+ */ + public static final String RPC_SECURE_CLIENT_TRUST_NAME_NICK = "secureClientCertValidate"; + + + /** + * Default for Certificate validation Method + */ + public static final int RPC_DEFAULT_SECURE_CLIENT_TRUST_NAME = 1; + /** * If this property is set and equals "false", do not attempt to set enabled * protocol versions (SSLSocket.setEnabledProtocols()) for the connection diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcServer.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcServer.java index 20d4b50..13862f0 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcServer.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/RpcServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2009 Perforce Software Inc., All Rights Reserved. + * Copyright (c) 2009-2022 Perforce Software Inc., All Rights Reserved. */ package com.perforce.p4java.impl.mapbased.rpc; @@ -42,6 +42,9 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.Charset; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -259,6 +262,44 @@ public abstract class RpcServer extends Server { protected String trustFilePath = null; + protected boolean validatedByChain = false; + + /** + * was the server ssl connection validated by chain? + * @return true if it's an ssl connection with valid chain + */ + public boolean isValidatedByChain() { + if (!isSecure()) { + return false; + } + return validatedByChain; + } + + protected boolean validatedByFingerprint = false; + + /*** + * was the server ssl connection validated by fingerprint? + * @return true if it's an ssl connection validated by fingerprint + */ + public boolean isValidatedByFingerprint() { + if (!isSecure()) { + return false; + } + return validatedByFingerprint; + } + + protected boolean validatedByHostname = false; + + /*** + * was the server ssl connection validated by hostname match? + * @return true if it's an ssl connection validated by "cert's CN" == "P4Port's hostname" + */ + public boolean isValidatedByHostname() { + if (!isSecure()) { + return false; + } + return validatedByHostname; + } protected int authFileLockTry = 0; protected long authFileLockDelay = 0; protected long authFileLockWait = 0; @@ -545,6 +586,7 @@ public String addTrust(final String fingerprintValue, final TrustOptions options } String originalFingerprint = rpcConnection.getFingerprint(); + PerforceMessages messages = clientTrust.getMessages(); String serverHostPort = getServerHostPort(); Object[] warningParams = {serverHostPort, originalFingerprint}; @@ -552,6 +594,7 @@ public String addTrust(final String fingerprintValue, final TrustOptions options warningParams); String newKeyWarning = messages.getMessage(CLIENT_TRUST_WARNING_NEW_KEY, warningParams); String serverIpPort = rpcConnection.getServerIpPort(); + boolean fingerprintExists = fingerprintExists(serverIpPort, FINGERPRINT_USER_NAME); boolean fingerprintMatches = fingerprintMatches(serverIpPort, FINGERPRINT_USER_NAME, originalFingerprint); @@ -560,22 +603,37 @@ public String addTrust(final String fingerprintValue, final TrustOptions options boolean fingerprintReplaceMatches = fingerprintMatches(serverIpPort, FINGERPRINT_REPLACEMENT_USER_NAME, originalFingerprint); + boolean fingerprintExistsHost = fingerprintExists(serverHostPort, FINGERPRINT_USER_NAME); + boolean fingerprintMatchesHost = fingerprintMatches(serverHostPort, FINGERPRINT_USER_NAME, + originalFingerprint); + boolean fingerprintReplaceExistsHost = fingerprintExists(serverHostPort, + FINGERPRINT_REPLACEMENT_USER_NAME); + boolean fingerprintReplaceMatchesHost = fingerprintMatches(serverHostPort, + FINGERPRINT_REPLACEMENT_USER_NAME, originalFingerprint); + + // auto refuse if (opts.isAutoRefuse()) { // new connection - if (!fingerprintExists) { + if (!fingerprintExists && !fingerprintExistsHost) { return newConnectionWarning; } // new key - if (!fingerprintMatches) { + // if (!fingerprintMatches) { + if (!fingerprintMatches && ! fingerprintMatchesHost) { return newKeyWarning; } } // check and use replacement newFingerprint - if (checkAndUseReplacementFingerprint(fingerprintExists, fingerprintMatches, - fingerprintReplaceExists, fingerprintReplaceMatches, rpcConnection)) { - + boolean established = checkAndUseReplacementFingerprint(serverIpPort, + fingerprintExists, fingerprintMatches, + fingerprintReplaceExists, fingerprintReplaceMatches, rpcConnection) ; + boolean establishedHost = checkAndUseReplacementFingerprint(serverHostPort, + fingerprintExists, fingerprintMatches, + fingerprintReplaceExists, fingerprintReplaceMatches, rpcConnection) ; + + if (established || establishedHost) { return messages.getMessage(CLIENT_TRUST_ALREADY_ESTABLISHED); } @@ -584,6 +642,7 @@ public String addTrust(final String fingerprintValue, final TrustOptions options String newFingerprint = firstNonBlank(fingerprintValue, originalFingerprint); String trustAddedInfo = messages.getMessage(CLIENT_TRUST_ADDED, new Object[]{serverHostPort, serverIpPort}); + // new connection if (installFingerprintIfNewConnection(fingerprintExists, rpcConnection, opts, fingerprintUser, newFingerprint)) { @@ -600,7 +659,15 @@ public String addTrust(final String fingerprintValue, final TrustOptions options if (fingerprintMatches && isNotBlank(fingerprintValue)) { // install newFingerprint - clientTrust.installFingerprint(serverIpPort, fingerprintUser, newFingerprint); + int sslClientTrustName = RpcPropertyDefs.getPropertyAsInt(this.props, + RpcPropertyDefs.RPC_SECURE_CLIENT_TRUST_NAME_NICK, + RpcPropertyDefs.RPC_DEFAULT_SECURE_CLIENT_TRUST_NAME ); + if (sslClientTrustName <=1 ) { + clientTrust.installFingerprint(serverIpPort, fingerprintUser, newFingerprint); + } + if ( sslClientTrustName >= 1) { + clientTrust.installFingerprint(getServerHostPort(), fingerprintUser, newFingerprint); + } return trustAddedInfo; } @@ -643,9 +710,18 @@ public String removeTrust(final TrustOptions opts) throws P4JavaException { } // remove the fingerprint from the trust file - clientTrust.removeFingerprint(serverIpPort, fingerprintUser); - return message + messages.getMessage(CLIENT_TRUST_REMOVED, - new Object[]{getServerHostPort(), serverIpPort}); + if (fingerprintExists(serverIpPort, fingerprintUser)) { + clientTrust.removeFingerprint(serverIpPort, fingerprintUser); + message += messages.getMessage(CLIENT_TRUST_REMOVED, + new Object[]{getServerHostPort(), serverIpPort}); + } + // remove the host entry + if (fingerprintExists(getServerHostPort(), fingerprintUser)) { + clientTrust.removeFingerprint(getServerHostPort(), fingerprintUser); + message += (message.length()>0 ? " ": "") + messages.getMessage(CLIENT_TRUST_REMOVED, + new Object[]{getServerHostPort(), getServerHostPort()}); + } + return message; } finally { closeQuietly(rpcConnection); } @@ -851,7 +927,8 @@ public ServerStatus init(final String host, final int port, final Properties pro return init(host, port, props, null); } - private boolean checkAndUseReplacementFingerprint(final boolean fingerprintExists, + private boolean checkAndUseReplacementFingerprint(final String serverKey, + final boolean fingerprintExists, final boolean fingerprintMatches, final boolean fingerprintReplaceExists, final boolean fingerprintReplaceMatches, final RpcConnection rpcConnection) throws TrustException { @@ -859,10 +936,10 @@ private boolean checkAndUseReplacementFingerprint(final boolean fingerprintExist if ((!fingerprintExists || !fingerprintMatches) && (fingerprintReplaceExists && fingerprintReplaceMatches)) { // Install/override newFingerprint - clientTrust.installFingerprint(rpcConnection.getServerIpPort(), FINGERPRINT_USER_NAME, + clientTrust.installFingerprint(serverKey, FINGERPRINT_USER_NAME, rpcConnection.getFingerprint()); // Remove the replacement - clientTrust.removeFingerprint(rpcConnection.getServerIpPort(), + clientTrust.removeFingerprint(serverKey, FINGERPRINT_REPLACEMENT_USER_NAME); return true; @@ -870,15 +947,70 @@ private boolean checkAndUseReplacementFingerprint(final boolean fingerprintExist return false; } + /** + * Check Server Trust + *

+ * Certificate Validation depends on RPC_SSL_CLIENT_CERT_VALIDATE_NICK. + *

+ * Self-signed certs use only a fingerprint comparison after checking the cert's dates. + */ + public void trustConnectionCheck(final RpcConnection rpcConnection) throws ConnectionException { + + int sslCertMethod = RpcPropertyDefs.getPropertyAsInt(this.props, + RpcPropertyDefs.RPC_SECURE_CLIENT_CERT_VALIDATE_NICK, + RpcPropertyDefs.RPC_DEFAULT_SECURE_CLIENT_CERT_VALIDATE); + // 0 = fingerprint, 1=chain then fingerprint, 2=hostname check only. + if (sslCertMethod < 0 || sslCertMethod > 2) { + sslCertMethod = RpcPropertyDefs.RPC_DEFAULT_SECURE_CLIENT_CERT_VALIDATE; + } + if ( ! rpcConnection.isSelfSigned() && sslCertMethod == 1 ) { + // validate the chain. + try { + ClientTrust.validateServerChain(rpcConnection.getServerCerts(), getServerHostPort().split(":")[0]); + validatedByChain = true; + + return; + } catch (Exception ce) { + boolean x = ce instanceof CertificateException; + // TODO: logging? - failure to validate cert chain so fallback to fingerprint. + // System.out.println("Fails cert chain: " + ce.getMessage()); + } + + } else if (! rpcConnection.isSelfSigned() && sslCertMethod == 2) { + // validate the P4PORT matches the CN. + X509Certificate[] certs = rpcConnection.getServerCerts(); + try { + ClientTrust.verifyCertificateSubject(certs[0], this.serverHost); + validatedByHostname = true ; + return; + } catch (Exception e) { + // TODO: logging - requested a subject match verification but didn't match. + } + } + + checkFingerprint(rpcConnection); + validatedByFingerprint = rpcConnection.isTrusted(); + } /** * Check the fingerprint of the Perforce server SSL connection */ protected void checkFingerprint(final RpcConnection rpcConnection) throws ConnectionException { + if (nonNull(rpcConnection) && rpcConnection.isSecure() && !rpcConnection.isTrusted()) { + + try { + if (rpcConnection.getServerCerts() .length > 0) { + ClientTrust.verifyCertificateDates(rpcConnection.getServerCerts()[0]); + } + } catch (CertificateException e) { + throw new ConnectionException(e); + } + String fingerprint = rpcConnection.getFingerprint(); throwConnectionExceptionIfConditionFails(isNotBlank(fingerprint), "Null fingerprint for this Perforce SSL connection"); + // look for IP in trust file String serverIpPort = rpcConnection.getServerIpPort(); boolean fingerprintExists = fingerprintExists(serverIpPort, FINGERPRINT_USER_NAME); boolean fingerprintReplaceExist = fingerprintExists(serverIpPort, @@ -892,14 +1024,31 @@ protected void checkFingerprint(final RpcConnection rpcConnection) throws Connec boolean isNotEstablished = (!fingerprintExists && !fingerprintReplaceExist) || (!fingerprintExists && !fingerprintReplaceMatches); - throwTrustExceptionIfConditionIsTrue(isNotEstablished, rpcConnection, NEW_CONNECTION, + // look for host:port in trust file + String serverHost = getServerHostPort(); + boolean fingerprintExistsHost = fingerprintExists(serverHost, FINGERPRINT_USER_NAME); + boolean fingerprintReplaceExistHost = fingerprintExists(serverHost, + FINGERPRINT_REPLACEMENT_USER_NAME); + + boolean fingerprintMatchesHost = clientTrust.fingerprintMatches(serverHost, + FINGERPRINT_USER_NAME, fingerprint); + boolean fingerprintReplaceMatchesHost = fingerprintMatches(serverHost, + FINGERPRINT_REPLACEMENT_USER_NAME, fingerprint); + + boolean isNotEstablishedHost = (!fingerprintExistsHost && !fingerprintReplaceExistHost) + || (!fingerprintExistsHost && !fingerprintReplaceMatchesHost); + + // first check: is either IP or host found? + throwTrustExceptionIfConditionIsTrue(isNotEstablished && isNotEstablishedHost, rpcConnection, NEW_CONNECTION, CLIENT_TRUST_WARNING_NOT_ESTABLISHED, CLIENT_TRUST_EXCEPTION_NEW_CONNECTION); - boolean isNewKey = !fingerprintMatches && !fingerprintReplaceMatches; - throwTrustExceptionIfConditionIsTrue(isNewKey, rpcConnection, NEW_KEY, + boolean isNewKey = (!fingerprintMatches && !fingerprintReplaceMatches); + boolean isNewKeyHost = (!fingerprintMatchesHost && !fingerprintReplaceMatchesHost); + + throwTrustExceptionIfConditionIsTrue(isNewKey && isNewKeyHost, rpcConnection, NEW_KEY, CLIENT_TRUST_WARNING_NEW_KEY, CLIENT_TRUST_EXCEPTION_NEW_KEY); - // Use replacement fingerprint + // Use replacement fingerprint for serverIP if ((!fingerprintExists || !fingerprintMatches) && (fingerprintReplaceExist && fingerprintReplaceMatches)) { // Install/override fingerprint @@ -907,6 +1056,14 @@ protected void checkFingerprint(final RpcConnection rpcConnection) throws Connec // Remove the replacement clientTrust.removeFingerprint(serverIpPort, FINGERPRINT_REPLACEMENT_USER_NAME); } + // Use replacement fingerprint for hostname + if ((!fingerprintExistsHost || !fingerprintMatchesHost) + && (fingerprintReplaceExistHost && fingerprintReplaceMatchesHost)) { + // Install/override fingerprint + clientTrust.installFingerprint(serverHost, FINGERPRINT_USER_NAME, fingerprint); + // Remove the replacement + clientTrust.removeFingerprint(serverHost, FINGERPRINT_REPLACEMENT_USER_NAME); + } // Trust this connection rpcConnection.setTrusted(true); @@ -928,7 +1085,6 @@ private void throwTrustExceptionIfConditionIsTrue(final boolean expression, final String warningMessageKey, final String exceptionMessageKey) throws TrustException { if (expression) { - throwTrustException(rpcConnection, type, warningMessageKey, exceptionMessageKey); } } @@ -1081,9 +1237,20 @@ private boolean installNewFingerprintIfIsAutoAccept(final RpcConnection rpcConne final String newFingerprint) throws TrustException { if (trustOptions.isAutoAccept()) { + int sslClientTrustName = RpcPropertyDefs.getPropertyAsInt(this.props, + RpcPropertyDefs.RPC_SECURE_CLIENT_TRUST_NAME_NICK, + RpcPropertyDefs.RPC_DEFAULT_SECURE_CLIENT_TRUST_NAME ); // install newFingerprint - clientTrust.installFingerprint(rpcConnection.getServerIpPort(), fingerprintUser, - newFingerprint); + if (sslClientTrustName == 0 || sslClientTrustName == 1) { + clientTrust.installFingerprint(rpcConnection.getServerIpPort(), fingerprintUser, + newFingerprint); + } + if (sslClientTrustName == 1 || sslClientTrustName == 2) { + String serverHostNamePort = rpcConnection.getServerHostNamePort(); + if (serverHostNamePort != null) { + clientTrust.installFingerprint(serverHostNamePort, fingerprintUser, newFingerprint); + } + } return true; } @@ -1112,15 +1279,15 @@ private void throwTrustException(final RpcConnection rpcConnection, * * @return - fingerprint or null if not found. */ - public Fingerprint loadFingerprint(final String serverIpPort, final String fingerprintUser) { + public Fingerprint loadFingerprint(final String serverKey, final String fingerprintUser) { - if (isBlank(serverIpPort) || isBlank(fingerprintUser)) { + if (isBlank(serverKey) || isBlank(fingerprintUser)) { return null; } Fingerprint fingerprint = null; try { - fingerprint = FingerprintsHelper.getFingerprint(fingerprintUser, serverIpPort, + fingerprint = FingerprintsHelper.getFingerprint(fingerprintUser, serverKey, trustFilePath); } catch (IOException e) { Log.error(e.getMessage()); @@ -1440,7 +1607,7 @@ protected boolean useTags(String cmdName, String[] cmdArgs, Map } /** - * Return true iff we should be performing server -> client file write I/O + * Return true if we should be performing server -> client file write I/O * operations in place for this command. *

*

@@ -1456,4 +1623,5 @@ protected boolean writeInPlace(String cmdName) { return cmdName.equalsIgnoreCase(CmdSpec.SYNC.toString()) && Boolean.valueOf(writeInPlaceKeyPropertyValue); } + } diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/connection/RpcConnection.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/connection/RpcConnection.java index f49b507..9af3ada 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/connection/RpcConnection.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/connection/RpcConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2009 Perforce Software Inc., All Rights Reserved. + * Copyright 2009 - 2022 Perforce Software Inc., All Rights Reserved. */ package com.perforce.p4java.impl.mapbased.rpc.connection; @@ -29,6 +29,8 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -120,6 +122,24 @@ public abstract class RpcConnection { protected boolean secure = false; protected String fingerprint = null; protected boolean trusted = false; + protected Certificate[] serverCerts = null; + + public X509Certificate[] getServerCerts() { + X509Certificate[] certs; + if ( serverCerts != null) { + certs = new X509Certificate[serverCerts.length]; + for (int i = 0; i < certs.length; i++) { + certs[i] = (X509Certificate) serverCerts[i]; + } + } else { + certs = new X509Certificate[0]; + } + return certs; + } + protected boolean selfSigned = true; + public boolean isSelfSigned() { + return selfSigned; + } /** * Create a Perforce RPC connection to a given host and port number pair. @@ -199,6 +219,8 @@ public RpcConnection(@Nonnull String serverHost, int serverPort, Properties prop */ public abstract String getServerIpPort(); + public abstract String getServerHostNamePort(); + /** * Get the client's IP and port used for the RPC connection. * diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/func/client/ClientTrust.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/func/client/ClientTrust.java index 0a7cada..b95e854 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/func/client/ClientTrust.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/func/client/ClientTrust.java @@ -3,11 +3,28 @@ */ package com.perforce.p4java.impl.mapbased.rpc.func.client; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import com.perforce.p4java.exception.ConfigException; import com.perforce.p4java.exception.NullPointerError; @@ -16,8 +33,16 @@ import com.perforce.p4java.messages.PerforceMessages; import com.perforce.p4java.server.Fingerprint; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + /** * Handle the client trust and fingerprint for Perforce SSL connections. + * + * This also include methods to assist in validating a certificate path. + * We trust all certificates but save the certificates for + * later checking with methods in this class. */ public class ClientTrust { @@ -49,9 +74,11 @@ public class ClientTrust { public static final String CLIENT_TRUST_INSTALL_EXCEPTION = "client.trust.install.exception"; public static final String CLIENT_TRUST_UNINSTALL_EXCEPTION = "client.trust.uninstall.exception"; + public static final String SSL_CLIENT_TRUST_BADDATE= "client.trust.cert.bad.date.exception"; + public static final String SSL_CLIENT_TRUST_BADHOST = "client.trust.cert.bad.host.exception"; private RpcServer rpcServer = null; - private PerforceMessages messages = new PerforceMessages( + private static PerforceMessages messages = new PerforceMessages( ClientTrust.CLIENT_TRUST_MESSAGES); /** @@ -136,14 +163,14 @@ public void removeFingerprint(String serverIpPort, String fingerprintUser) throw /** * Check if the fingerprint exists for the specified server IP and port * - * @param serverIpPort - * the serverIpPort + * @param serverKey + * the serverIpPort or serverHostName * @param fingerprintUser * the fingerprintUser * @return true, if successful */ - public boolean fingerprintExists(String serverIpPort, String fingerprintUser) { - if (serverIpPort == null) { + public boolean fingerprintExists(String serverKey, String fingerprintUser) { + if (serverKey == null) { throw new NullPointerError( "null serverIpPort passed to the ClientTrust fingerprintExists method"); } @@ -151,23 +178,23 @@ public boolean fingerprintExists(String serverIpPort, String fingerprintUser) { throw new NullPointerError( "null fingerprintUser passed to the ClientTrust fingerprintExists method"); } - return (rpcServer.loadFingerprint(serverIpPort, fingerprintUser) != null); + return (rpcServer.loadFingerprint(serverKey, fingerprintUser) != null); } /** * Check if the fingerprint for the specified server IP and port matches the * one in trust file. - * - * @param serverIpPort - * the serverIpPort + * + * @param serverKey + * the serverIpPort or serverHostName * @param fingerprintUser * the fingerprintUser * @param fingerprint * the fingerprint * @return true, if successful */ - public boolean fingerprintMatches(String serverIpPort, String fingerprintUser, String fingerprint) { - if (serverIpPort == null) { + public boolean fingerprintMatches(String serverKey, String fingerprintUser, String fingerprint) { + if (serverKey == null) { throw new NullPointerError( "null serverIpPort passed to the ClientTrust fingerprintMatches method"); } @@ -179,9 +206,9 @@ public boolean fingerprintMatches(String serverIpPort, String fingerprintUser, S throw new NullPointerError( "null fingerprint passed to the ClientTrust fingerprintMatches method"); } - if (fingerprintExists(serverIpPort, fingerprintUser)) { + if (fingerprintExists(serverKey, fingerprintUser)) { Fingerprint existingFingerprint = rpcServer - .loadFingerprint(serverIpPort, fingerprintUser); + .loadFingerprint(serverKey, fingerprintUser); if (existingFingerprint != null && existingFingerprint.getFingerprintValue() != null) { if (fingerprint.equalsIgnoreCase(existingFingerprint @@ -257,4 +284,184 @@ public static String convert2Hex(byte[] data) { public PerforceMessages getMessages() { return messages; } + + /** + * We assume a JVM will only use one trust store.
+ * System.setProperty() is often prohibited by java security policy so you can't + * change the trustore property mid application. + */ + private static Set trustedCAs; + + /** + * Gets the root CAs in the trust store, either the default truststore or as + * specified by javax.net.ssl.trustStore/javax.net.ssl.trustStorePassword. + * root CAs are cached. + */ + public static Set getTrustedCAs() + throws NoSuchAlgorithmException, KeyStoreException, InvalidAlgorithmParameterException { + return getTrustedCAs(false); + } + /** + * Gets the root CAs from the trust store, either the default truststore or as + * specified by javax.net.ssl.trustStore/javax.net.ssl.trustStorePassword. + * + * @param refreshCache force retrieve from truststore + * @return + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws InvalidAlgorithmParameterException + */ + public static synchronized Set getTrustedCAs(boolean refreshCache) + throws NoSuchAlgorithmException, KeyStoreException, InvalidAlgorithmParameterException { + if (! refreshCache && trustedCAs != null) { + return trustedCAs; + } + X509TrustManager x509tm = getDefaultX509TrustManager(); + trustedCAs = new HashSet(); + for (X509Certificate cert : x509tm.getAcceptedIssuers()) { + trustedCAs.add(new TrustAnchor(cert, null)); + } + return trustedCAs; + } + /** + * Get the system default trust manager {@link X509TrustManager} + */ + public static X509TrustManager getDefaultX509TrustManager() throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + tmf.init((KeyStore) null); + for (TrustManager trustMgr : tmf.getTrustManagers()) { + if (trustMgr instanceof X509TrustManager) { + return (X509TrustManager) trustMgr; + } + } + throw new IllegalStateException("X509TrustManager is not found"); + } + + /** + * Check the certificate chain. + * + * @param certs the certificates from p4d handshake. + * @throws CertificateException if the validation fails + */ + public static void validateServerChain(X509Certificate[] certs, String refName) + throws CertificateException { + + // workaround for bug P4-22041: + // remove duplicates at the end of the chain. + // do not disturb order of certs. + List certList = new ArrayList<>(); + for (X509Certificate cert : certs) { + if (certList.contains(cert)) { + continue; + } + certList.add(cert); + } + + try { + verifyCertificateSubject(certs[0], refName); + verifyCertificateDates(certs[0]); + + CertPathValidator certPathValidator = CertPathValidator.getInstance("PKIX"); + CertificateFactory certFactory = CertificateFactory.getInstance("X509"); + CertPath path = certFactory.generateCertPath(certList); + + // parameters used for validating certs. + PKIXParameters pkixParameters = new PKIXParameters(getTrustedCAs()); + pkixParameters.setRevocationEnabled(false); // TODO: configurable? + + PKIXCertPathValidatorResult valDetails = (PKIXCertPathValidatorResult) certPathValidator.validate(path, pkixParameters); + + // TODO logging for validation: ssl=3 System.out.println("result=" + valDetails.toString()); + + } catch (GeneralSecurityException e) { + // example: java.security.cert.CertPathValidatorException: path does not chain with any of the trust anchors + throw new CertificateException(e); + } + } + + /** + * Check the certificate Not Before and Not After dates + * @param cert + * @throws CertificateException + */ + public static void verifyCertificateDates(X509Certificate cert) throws CertificateException { + Date after= cert.getNotAfter(); + Date before = cert.getNotBefore() ; + Date now = new Date(); + + if (now.before(before)) { + throw new CertificateException(messages.getMessage(SSL_CLIENT_TRUST_BADDATE, + new Object[] { "before", before})); + } + if (now.after(after)) { + throw new CertificateException(messages.getMessage(SSL_CLIENT_TRUST_BADDATE, + new Object[] { "after", after})); + } + return; + } + + /** + * Verify the request's hostname to that in the certificate. + * + * @param cert certificate + * @param hostName Host name + * @throws CertificateParsingException + * @throws CertificateException + */ + public static void verifyCertificateSubject(X509Certificate cert, String hostName) throws CertificateParsingException, CertificateException { + + // check SANs first, https://www.rfc-editor.org/rfc/rfc6125#section-6.4.3 + for (List entry : cert.getSubjectAlternativeNames()) { + final int type = ((Integer) entry.get(0)).intValue(); + // DNS or IP + if (type == 2 || type == 7) { + if ( matchSubject((String)entry.get(1), hostName)) { + return; + } + } + } + + // check the CN. I think RFC 6125 says we shouldn't, but p4api compares it. + String cn = cert.getSubjectDN().getName(); + if (cn.startsWith("CN=")) { + cn = cn.substring(3); + } + if ( matchSubject(cn, hostName)) { + return; + } + + // not expected to be here, so be nice and tell + // what CN values the cert had for the exception msg. + StringBuilder sb = new StringBuilder(); + for (List entry : cert.getSubjectAlternativeNames()) { + final int type = ((Integer) entry.get(0)).intValue(); + if (type == 2 || type == 7) { + sb.append((String)entry.get(1) + ","); + } + } + sb.append(cn); + throw new CertificateException(messages.getMessage(SSL_CLIENT_TRUST_BADHOST,new Object[]{hostName, sb})); + } + + /** + * Check to see if a cert's subject matches with a reference name (e.g., hostname in P4PORT) + * Note that the subject may contain a leading wildcard "*.".
+ * + * @param subject - certificate's subject name (a SAN value or CN) + * @param refName reference name to compare. + * @return true if matches. + */ + private static boolean matchSubject(String subject, String refName) { + if ( subject.startsWith("*.") ) { + subject = subject.substring(1); // remove "*" + int firstDot = refName.indexOf("."); // remove hostname? + firstDot = (firstDot >= 0 ) ? firstDot : 0; + if (subject.equalsIgnoreCase(refName.substring(firstDot))) { + return true; + } + } + return subject.equalsIgnoreCase(refName); + } + } \ No newline at end of file diff --git a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/stream/RpcStreamConnection.java b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/stream/RpcStreamConnection.java index 8178115..93eec60 100644 --- a/src/main/java/com/perforce/p4java/impl/mapbased/rpc/stream/RpcStreamConnection.java +++ b/src/main/java/com/perforce/p4java/impl/mapbased/rpc/stream/RpcStreamConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2009 Perforce Software Inc., All Rights Reserved. + * Copyright 2009-2022 Perforce Software Inc., All Rights Reserved. */ package com.perforce.p4java.impl.mapbased.rpc.stream; @@ -240,7 +240,6 @@ private void getIpAddressFromSocketConnection() { } this.ourPort = socket.getLocalPort(); } - } } @@ -260,26 +259,33 @@ private void initSSL() throws ConnectionException { */ throwConnectionExceptionIfConditionFails(sslSession.isValid(), "Error occurred during the SSL handshake: invalid SSL session"); + // Get the certificates - Certificate[] serverCerts = sslSession.getPeerCertificates(); + serverCerts = sslSession.getPeerCertificates(); throwConnectionExceptionIfConditionFails( nonNull(serverCerts) && (serverCerts.length != 0) && nonNull(serverCerts[0]), "Error occurred during the SSL handshake: no certificate retrieved from SSL session"); + X509Certificate siteCert = (X509Certificate) serverCerts[0]; + // Check that the certificate is currently valid. Check the // current date and time are within the validity period given // in the certificate. - ((X509Certificate) serverCerts[0]).checkValidity(); + siteCert.checkValidity(); // Get the public key from the first certificate - PublicKey serverPubKey = serverCerts[0].getPublicKey(); + PublicKey serverPubKey = siteCert.getPublicKey(); throwConnectionExceptionIfConditionFails(nonNull(serverPubKey), "Error occurred during the SSL handshake: no public key retrieved from server certificate"); + // check if it's a self signed cert. + selfSigned = siteCert.getSubjectDN().getName().equals(siteCert.getIssuerDN().getName()); + // Generate the fingerprint fingerprint = ClientTrust.generateFingerprint(serverPubKey); + } catch (CertificateExpiredException e) { throwConnectionException(e, "Error occurred during the SSL handshake: certificate expired:"); @@ -323,6 +329,9 @@ public String getServerIpPort() { return serverIpPort; } + public String getServerHostNamePort() { + return hostName + ":" + String.valueOf(hostPort); + } /** * @see com.perforce.p4java.impl.mapbased.rpc.connection.RpcConnection#getClientIpPort() */ diff --git a/src/main/resources/com/perforce/p4java/messages/ClientTrustMessages.properties b/src/main/resources/com/perforce/p4java/messages/ClientTrustMessages.properties index 73f6251..7f1582f 100644 --- a/src/main/resources/com/perforce/p4java/messages/ClientTrustMessages.properties +++ b/src/main/resources/com/perforce/p4java/messages/ClientTrustMessages.properties @@ -54,3 +54,9 @@ Error occurred while installing fingerprint {0} for Perforce server ''{1}'' ({2} # uninstall fingerprint exception client.trust.uninstall.exception=\ Error occurred while uninstalling fingerprint for Perforce server ''{0}'' ({1}) from trust file. + +client.trust.cert.bad.date.exception=\ +Current date is {0} certificate''s not {0} date of {1} + +client.trust.cert.bad.host.exception=\ +Request to ''{0}'' does not match certificate''s list of names: {1} \ No newline at end of file