Merge remote-tracking branch

'origin/GP-3050-2935-ghidra1_ServerAddressAndConnectTimeout--SQUASHED'
(Closes #4924, Closes #4928)
This commit is contained in:
ghidra1
2023-02-03 14:27:05 -05:00
11 changed files with 139 additions and 91 deletions
@@ -32,7 +32,6 @@ import ghidra.framework.main.FrontEndTool;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.dialog.ExtensionUtils;
import ghidra.framework.project.DefaultProjectManager;
import ghidra.framework.remote.InetNameLookup;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramDB;
import ghidra.util.*;
@@ -94,9 +93,6 @@ public class GhidraRun implements GhidraLaunchable {
});
};
// Automatically disable reverse name lookup if failure occurs
InetNameLookup.setDisableOnFailure(true);
// Start main thread in GhidraThreadGroup
Thread mainThread = new Thread(new GhidraThreadGroup(), mainTask, "Ghidra");
mainThread.start();
@@ -25,9 +25,9 @@ import java.util.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.framework.remote.InetNameLookup;
import ghidra.framework.store.local.IndexedLocalFileSystem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.server.remote.InetNameLookup;
import ghidra.server.remote.RepositoryServerHandleImpl;
import ghidra.util.NamingUtilities;
import ghidra.util.StringUtilities;
@@ -751,6 +751,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
// keystore has not been identified - use self-signed certificate
ApplicationKeyManagerFactory.setDefaultIdentity(
new X500Principal("CN=GhidraServer"));
ApplicationKeyManagerFactory.addSubjectAlternativeName(hostname);
}
if (!ApplicationKeyManagerFactory.initialize()) {
log.fatal("Failed to initialize PKI/SSL keystore");
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.remote;
package ghidra.server.remote;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -18,7 +18,6 @@ package ghidra.framework.client;
import java.awt.Component;
import java.io.IOException;
import java.net.Authenticator;
import java.net.UnknownHostException;
import java.rmi.*;
import java.security.GeneralSecurityException;
import java.util.Arrays;
@@ -50,7 +49,7 @@ public class ClientUtil {
/**
* Set client authenticator
* @param authenticator
* @param authenticator client authenticator instance
*/
public static synchronized void setClientAuthenticator(ClientAuthenticator authenticator) {
clientAuthenticator = authenticator;
@@ -106,14 +105,6 @@ public class ClientUtil {
// ensure that default callback is setup if possible
getClientAuthenticator();
host = host.trim().toLowerCase();
try {
host = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
Msg.warn(ClientUtil.class, "Failed to resolve hostname for " + host);
}
if (port <= 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
@@ -145,22 +136,14 @@ public class ClientUtil {
* Eliminate the specified repository server from the connection cache
* @param host host name or IP address
* @param port port (0: use default port)
* @throws IOException
*/
public static void clearRepositoryAdapter(String host, int port) throws IOException {
host = host.trim().toLowerCase();
String hostAddr = host;
try {
hostAddr = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
throw new IOException("Repository server lookup failed: " + host);
}
public static void clearRepositoryAdapter(String host, int port) {
if (port == 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
ServerInfo server = new ServerInfo(hostAddr, port);
ServerInfo server = new ServerInfo(host, port);
RepositoryServerAdapter serverAdapter = serverHandles.remove(server);
if (serverAdapter != null) {
serverAdapter.disconnect();
@@ -170,6 +153,7 @@ public class ClientUtil {
/**
* Returns default user login name. Actual user name used by repository
* should be obtained from RepositoryServerAdapter.getUser
* @return default user name
*/
public static String getUserName() {
String name = SystemUtilities.getUserName();
@@ -372,7 +356,7 @@ public class ClientUtil {
* @param parent dialog parent
* @param handle server handle
* @param serverInfo server information
* @throws IOException
* @throws IOException if error occurs while updating password
*/
public static void changePassword(Component parent, RepositoryServerHandle handle,
String serverInfo) throws IOException {
@@ -451,7 +435,7 @@ public class ClientUtil {
sigCb.getRecognizedAuthorities(), sigCb.getToken());
sigCb.sign(signedToken.certChain, signedToken.signature);
Msg.info(ClientUtil.class, "PKI Authenticating to " + serverName + " as user '" +
signedToken.certChain[0].getSubjectDN() + "'");
signedToken.certChain[0].getSubjectX500Principal() + "'");
}
catch (Exception e) {
String msg = e.getMessage();
@@ -18,10 +18,12 @@ package ghidra.framework.client;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.cert.Certificate;
import java.util.HashSet;
import javax.net.ssl.SSLHandshakeException;
@@ -46,6 +48,8 @@ import ghidra.util.task.*;
*/
class ServerConnectTask extends Task {
private static final int LIVENESS_CHECK_TIMEOUT_MS = 3000;
private ServerInfo server;
//private String defaultUserID;
private boolean allowLoginRetry;
@@ -98,6 +102,7 @@ class ServerConnectTask extends Task {
* if handle is null after running task. If both the exception
* and handle are null, it implies the connection attempt was cancelled
* by the user.
* @return exception which occured during a failed connection attempt, or null
*/
Exception getException() {
return exc;
@@ -123,16 +128,6 @@ class ServerConnectTask extends Task {
return subj;
}
private static String getPreferredHostname(String name) {
try {
return InetNameLookup.getCanonicalHostName(name);
}
catch (UnknownHostException e) {
Msg.warn(ServerConnectTask.class, "Failed to resolve hostname for " + name);
}
return name;
}
private static boolean isSSLHandshakeCancelled(SSLHandshakeException e) throws IOException {
if (e.getMessage().indexOf("bad_certificate") > 0) {
if (ApplicationKeyManagerFactory.getPreferredKeyStore() == null) {
@@ -154,8 +149,8 @@ class ServerConnectTask extends Task {
* @param server server information
* @param monitor cancellable monitor
* @return Ghidra Server Handle object
* @throws IOException
* @throws CancelledException
* @throws IOException if a connection error occurs
* @throws CancelledException if connection attempt was cancelled
*/
public static GhidraServerHandle getGhidraServerHandle(ServerInfo server, TaskMonitor monitor)
throws IOException, CancelledException {
@@ -163,6 +158,7 @@ class ServerConnectTask extends Task {
GhidraServerHandle gsh = null;
boolean canCancel = monitor.isCancelEnabled(); // original state
try {
// Test SSL Handshake to ensure that user is able to decrypt keystore.
// This is intended to work around an RMI issue where a continuous
// retry condition can occur when a user cancels the password entry
@@ -172,17 +168,10 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(false);
monitor.setMessage("Connecting...");
Registry reg;
try {
// attempt to connect with older Ghidra Server registry without using SSL/TLS
reg = LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber());
checkServerBindNames(reg);
}
catch (IOException e) {
reg = LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(),
Registry reg =
LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(),
new SslRMIClientSocketFactory());
checkServerBindNames(reg);
}
checkServerBindNames(reg);
gsh = (GhidraServerHandle) reg.lookup(GhidraServerHandle.BIND_NAME);
gsh.checkCompatibility(GhidraServerHandle.INTERFACE_VERSION);
@@ -241,20 +230,17 @@ class ServerConnectTask extends Task {
/**
* Attempts server connection and completes any necessary authentication.
* @param defaultUserID
* @param defaultUserID default user ID (actual ID used established during authentication)
* @param monitor task monitor for connection cancellation
* @return server handle or null if authentication or connection attempt was cancelled by user
* @throws IOException
* @throws LoginException
* @throws IOException if server connection fails
* @throws LoginException login failure
*/
private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID,
TaskMonitor monitor)
throws IOException, LoginException, CancelledException {
GhidraServerHandle gsh = getGhidraServerHandle(server, monitor);
if (gsh == null) {
return null;
}
Callback[] callbacks = null;
try {
@@ -275,7 +261,6 @@ class ServerConnectTask extends Task {
}
}
String serverName = getPreferredHostname(server.getServerName());
AnonymousCallback onlyAnonymousCb = null;
while (true) {
try {
@@ -298,8 +283,8 @@ class ServerConnectTask extends Task {
// SSH option only available in conjunction with password
// based authentication which will be used if SSH attempt fails
hasSSHSignatureCallback = false; // only try SSH once
ClientUtil.processSSHSignatureCallback(callbacks, serverName,
defaultUserID);
ClientUtil.processSSHSignatureCallback(callbacks,
server.getServerName(), defaultUserID);
}
else if (pkiSignatureCb != null) {
// when using PKI - no other authentication callback will be used
@@ -317,14 +302,15 @@ class ServerConnectTask extends Task {
}
loopOK = false; // only try once
ClientUtil.processSignatureCallback(serverName, pkiSignatureCb);
ClientUtil.processSignatureCallback(server.getServerName(),
pkiSignatureCb);
}
else {
// assume all other callback scenarios are password based
// anonymous option must be explicitly chosen over username/password
// when processing password callback
if (!ClientUtil.processPasswordCallbacks(callbacks, serverName,
defaultUserID, loginError)) {
if (!ClientUtil.processPasswordCallbacks(callbacks,
server.getServerName(), defaultUserID, loginError)) {
return null; // Cancelled by user
}
}
@@ -336,7 +322,7 @@ class ServerConnectTask extends Task {
gsh.getRepositoryServer(getLocalUserSubject(), callbacks);
if (rsh.isReadOnly()) {
Msg.showInfo(this, null, "Anonymous Server Login",
"You have been logged-in anonymously to " + serverName +
"You have been logged-in anonymously to " + server.getServerName() +
"\nRead-only permission is granted to repositories which allow anonymous access");
}
return rsh;
@@ -374,7 +360,30 @@ class ServerConnectTask extends Task {
}
}
private static void testServerSSLConnection(ServerInfo server, TaskMonitor monitor)
/**
* Socket implementation with very short connect timeout
*/
private static class FastConnectionFailSocket extends Socket {
FastConnectionFailSocket(String host, int port) throws UnknownHostException, IOException {
super(host, port);
}
public void connect(SocketAddress endpoint) throws IOException {
connect(endpoint, LIVENESS_CHECK_TIMEOUT_MS);
}
}
/**
* Initiate an SSLSocket connection in order to ensure that any neccesary client/server
* certificate validation is performed.
* @param server server to which connection should be verified. For the Ghidra Server
* this should correspond to the RMI Registry port {@link GhidraServerHandle#DEFAULT_PORT}.
* @param monitor connection task monitor
* @return certificate chain of server
* @throws IOException if connection failure occurs
* @throws CancelledException if connection attempt is cancelled
*/
private static Certificate[] testServerSSLConnection(ServerInfo server, TaskMonitor monitor)
throws IOException, CancelledException {
RMIServerPortFactory portFactory = new RMIServerPortFactory(server.getPortNumber());
@@ -384,7 +393,18 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(true);
monitor.setMessage("Checking Server Liveness...");
// Perform simple socket test connection with short timeout to verify connectivity.
try (Socket socket = new FastConnectionFailSocket(serverName, sslRmiPort);
ConnectCancelledListener cancelListener =
new ConnectCancelledListener(monitor, () -> forceClose(socket))) {
// do nothing - connect occurs during instantiation
}
finally {
monitor.checkCanceled(); // circumvent any IOException which may have occured
}
// Perform secure socket test connection to prime keystore use without RMI involvement
try (SSLSocket socket = (SSLSocket) factory.createSocket(serverName, sslRmiPort);
ConnectCancelledListener cancelListener =
new ConnectCancelledListener(monitor, () -> forceClose(socket))) {
@@ -392,6 +412,7 @@ class ServerConnectTask extends Task {
// which will give user ability to cancel without involving RMI which
// will avoid RMI reconnect attempts
socket.startHandshake();
return socket.getSession().getPeerCertificates();
}
finally {
monitor.checkCanceled(); // circumvent any IOException which may have occured
@@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +37,8 @@ public class ServerInfo implements Serializable {
}
/**
* Get the server name.
* Get the server hostname or IP address as originally specified.
* @return hostname or IP address as originally specified
*/
public String getServerName() {
return host;
@@ -46,6 +46,7 @@ public class ServerInfo implements Serializable {
/**
* Get the port number.
* @return port number
*/
public int getPortNumber() {
return portNumber;
@@ -21,7 +21,7 @@ import java.net.Socket;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.*;
import javax.net.ssl.*;
import javax.security.auth.x500.X500Principal;
@@ -62,6 +62,7 @@ public class ApplicationKeyManagerFactory {
private static KeyStorePasswordProvider customPasswordProvider;
private static X500Principal defaultIdentity;
private static List<String> subjectAlternativeNames;
private static ApplicationKeyManagerFactory instance;
@@ -182,16 +183,48 @@ public class ApplicationKeyManagerFactory {
/**
* Set the default self-signed principal identity to be used during initialization
* if no keystore defined. Current application key manager will be invalidated.
* @param identity if not null and a KeyStore path has not be set, this
* identity will be used to generate a self-signed certificate and private key
* (NOTE: this is intended for server use only when client will not be performing
* CA validation).
* @param identity if not null and a KeyStore path has not be set, this
* identity will be used to generate a self-signed certificate and private key
*/
public synchronized static void setDefaultIdentity(X500Principal identity) {
defaultIdentity = identity;
getKeyManagerWrapper().invalidateKey();
}
/**
* Add the optional self-signed subject alternative name to be used during initialization
* if no keystore defined. Current application key manager will be invalidated.
* (NOTE: this is intended for server use only when client will not be performing
* CA validation).
* @param subjectAltName name to be added to the current list of alternative subject names.
* A null value will clear all names currently set.
* name will be used to generate a self-signed certificate and private key
*/
public synchronized static void addSubjectAlternativeName(String subjectAltName) {
if (subjectAltName == null) {
subjectAlternativeNames = null;
}
else {
if (subjectAlternativeNames == null) {
subjectAlternativeNames = new ArrayList<>();
}
subjectAlternativeNames.add(subjectAltName);
}
getKeyManagerWrapper().invalidateKey();
}
/**
* Get the current list of subject alternative names to be used for a self-signed certificate
* if no keystore defined.
* @return list of subject alternative names to be used for a self-signed certificate
* if no keystore defined.
*/
public synchronized static List<String> getSubjectAlternativeName() {
return Collections.unmodifiableList(subjectAlternativeNames);
}
/**
* Initialize key manager if needed. Doing this explicitly independent of an SSL connection
* allows application to bail before initiating connection. This will get handshake failure
@@ -548,7 +581,7 @@ public class ApplicationKeyManagerFactory {
KeyStore selfSignedKeyStore =
ApplicationKeyManagerUtils.createKeyStore("defaultSigKey",
defaultIdentity.getName(), SELF_SIGNED_DURATION_DAYS, null, null, "JKS",
pwd);
subjectAlternativeNames, pwd);
keystoreData = new ProtectedKeyStoreData(selfSignedKeyStore, pwd);
isSelfSigned = true;
}
@@ -37,6 +37,7 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.IPAddress;
import generic.random.SecureRandomFactory;
import ghidra.util.Msg;
@@ -301,15 +302,19 @@ public class ApplicationKeyManagerUtils {
* @param alias entry alias with keystore
* @param dn distinguished name (e.g., "CN=Ghidra Test, O=Ghidra, OU=Test, C=US" )
* @param durationDays number of days which generated certificate should remain valid
* @param caEntry optional CA private key entry. If null, a self-signed CA certificate will be generated.
* @param caEntry optional CA private key entry. If null, a self-signed CA certificate will be
* generated.
* @param keyFile optional file to load/store resulting {@link KeyStore} (may be null)
* @param keystoreType support keystore type (e.g., "JKS", "PKCS12")
* @param subjectAlternativeNames an optional list of subject alternative names to be included
* in certificate (may be null)
* @param protectedPassphrase key and keystore protection password
* @return keystore containing newly generated certification with key pair
* @throws KeyStoreException if error occurs while updating keystore
*/
public static final KeyStore createKeyStore(String alias, String dn, int durationDays,
PrivateKeyEntry caEntry, File keyFile, String keystoreType, char[] protectedPassphrase)
PrivateKeyEntry caEntry, File keyFile, String keystoreType,
Collection<String> subjectAlternativeNames, char[] protectedPassphrase)
throws KeyStoreException {
PasswordProtection pp = new PasswordProtection(protectedPassphrase);
@@ -352,9 +357,8 @@ public class ApplicationKeyManagerUtils {
}
X509Certificate caX509Cert = (X509Certificate) caCert;
caX500Name =
new X500Name(caX509Cert.getSubjectDN().getName());
keyUsage = new KeyUsage(
KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
new X500Name(caX509Cert.getSubjectX500Principal().getName());
keyUsage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
issuerKey = caEntry.getPrivateKey();
}
Date notBefore = new Date();
@@ -362,18 +366,23 @@ public class ApplicationKeyManagerUtils {
Date notAfter = new Date(notBefore.getTime() + durationMs);
BigInteger serialNumber = new BigInteger(128, random);
// JcaX509ExtensionUtils x509Utils = new JcaX509ExtensionUtils();
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(caX500Name,
serialNumber, notBefore, notAfter, x500Name, bcPk);
certificateBuilder
// .addExtension(Extension.subjectKeyIdentifier, true, x509Utils.createSubjectKeyIdentifier(bcPk))
.addExtension(Extension.keyUsage, true, keyUsage);
certificateBuilder.addExtension(Extension.keyUsage, true, keyUsage);
if (subjectAlternativeNames != null && !subjectAlternativeNames.isEmpty()) {
List<GeneralName> nameList = new ArrayList<GeneralName>();
for (String altName : subjectAlternativeNames) {
int nameType =
IPAddress.isValid(altName) ? GeneralName.iPAddress : GeneralName.dNSName;
nameList.add(new GeneralName(nameType, altName));
}
GeneralName[] altNames = nameList.toArray(GeneralName[]::new);
certificateBuilder.addExtension(Extension.subjectAlternativeName, false,
new GeneralNames(altNames));
}
if (caEntry == null) {
certificateBuilder
.addExtension(Extension.basicConstraints, true, new BasicConstraints(1));
// .addExtension(Extension.authorityKeyIdentifier, true, x509Utils.createAuthorityKeyIdentifier(bcPk));
certificateBuilder.addExtension(Extension.basicConstraints, true,
new BasicConstraints(1));
}
ContentSigner contentSigner =
@@ -438,18 +447,21 @@ public class ApplicationKeyManagerUtils {
* @param caEntry optional CA private key entry. If null, a self-signed CA certificate will be generated.
* @param keyFile optional file to load/store resulting {@link KeyStore} (may be null)
* @param keystoreType support keystore type (e.g., "JKS", "PKCS12")
* @param subjectAlternativeNames an optional list of subject alternative names to be included
* in certificate (may be null)
* @param protectedPassphrase key and keystore protection password
* @return newly generated keystore entry with key pair
* @throws KeyStoreException if error occurs while updating keystore
*/
public static final PrivateKeyEntry createKeyEntry(String alias, String dn, int durationDays,
PrivateKeyEntry caEntry, File keyFile, String keystoreType, char[] protectedPassphrase)
PrivateKeyEntry caEntry, File keyFile, String keystoreType,
Collection<String> subjectAlternativeNames, char[] protectedPassphrase)
throws KeyStoreException {
PasswordProtection pp = new PasswordProtection(protectedPassphrase);
try {
KeyStore keyStore = createKeyStore(alias, dn, durationDays, caEntry, keyFile,
keystoreType, protectedPassphrase);
keystoreType, subjectAlternativeNames, protectedPassphrase);
return (PrivateKeyEntry) keyStore.getEntry(alias, pp);
}
catch (NoSuchAlgorithmException | UnrecoverableEntryException e) {
@@ -74,7 +74,7 @@ public class ApplicationKeyManagerFactoryTest extends AbstractGenericTest {
keystoreFile.delete();
ApplicationKeyManagerUtils.createKeyStore(ALIAS, TEST_IDENTITY, 2, null, keystoreFile,
"PKCS12", TEST_PWD.toCharArray());
"PKCS12", null, TEST_PWD.toCharArray());
ApplicationKeyManagerFactory.setKeyStorePasswordProvider(passwordProvider);
}
@@ -943,21 +943,21 @@ public class ServerTestUtil {
Msg.info(ServerTestUtil.class, "Generating self-signed CA cert: " + caPath);
PrivateKeyEntry caEntry =
ApplicationKeyManagerUtils.createKeyEntry("test-CA", TEST_PKI_CA_DN, 2, null, null,
"PKCS12", ApplicationKeyManagerFactory.DEFAULT_PASSWORD.toCharArray());
"PKCS12", null, ApplicationKeyManagerFactory.DEFAULT_PASSWORD.toCharArray());
ApplicationKeyManagerUtils.exportX509Certificates(caEntry.getCertificateChain(), caFile);
// Generate User/Client certificate and keystore
Msg.info(ServerTestUtil.class, "Generating test user key/cert (signed by test-CA, pwd: " +
TEST_PKI_USER_PASSPHRASE + "): " + userKeystorePath);
ApplicationKeyManagerUtils.createKeyEntry("test-sig", TEST_PKI_USER_DN, 2, caEntry,
userKeystoreFile, "PKCS12", TEST_PKI_USER_PASSPHRASE.toCharArray());
userKeystoreFile, "PKCS12", null, TEST_PKI_USER_PASSPHRASE.toCharArray());
// Generate Server certificate and keystore
Msg.info(ServerTestUtil.class, "Generating test server key/cert (signed by test-CA, pwd: " +
TEST_PKI_SERVER_PASSPHRASE + "): " + serverKeystorePath);
ApplicationKeyManagerUtils.createKeyEntry("test-sig", TEST_PKI_SERVER_DN, 2, caEntry,
serverKeystoreFile, "PKCS12", TEST_PKI_SERVER_PASSPHRASE.toCharArray());
serverKeystoreFile, "PKCS12", null, TEST_PKI_SERVER_PASSPHRASE.toCharArray());
}
/**