diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java
index eb6acfa1ef..3fd2ab4df1 100644
--- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java
+++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java
@@ -15,7 +15,7 @@
*/
package ghidra.server.security;
-import java.io.File;
+import java.io.*;
import java.util.*;
import javax.security.auth.Subject;
@@ -24,13 +24,25 @@ import javax.security.auth.callback.NameCallback;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
-import ch.ethz.ssh2.signature.*;
+import org.bouncycastle.crypto.CipherParameters;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+import org.bouncycastle.crypto.params.DSAKeyParameters;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+import org.bouncycastle.crypto.signers.*;
+import org.bouncycastle.util.Strings;
+
import ghidra.framework.remote.GhidraPrincipal;
import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.framework.remote.security.SSHKeyManager;
import ghidra.net.*;
import ghidra.server.UserManager;
+/**
+ * SSHAuthenticationModule
provides SHA1-RSA and SHA1-DSA signature-based authentication
+ * support using SSH public/private keys where user public keys are made available to the server.
+ * Module makes use of a {@link SSHSignatureCallback} object to convey the signature request to a
+ * client.
+ */
public class SSHAuthenticationModule {
private static final long MAX_TOKEN_TIME = 10000;
@@ -86,13 +98,49 @@ public class SSHAuthenticationModule {
return false;
}
+ /**
+ * Read UInt32 from SSH-encoded buffer.
+ * (modeled after org.bouncycastle.crypto.util.SSHBuffer.readU32())
+ * @param in data input stream
+ * @return integer value
+ * @throws IOException if IO error occurs reading input stream or inadequate
+ * bytes are available.
+ */
+ private static int sshBufferReadUInt32(ByteArrayInputStream in) throws IOException {
+ byte[] tmp = in.readNBytes(4);
+ if (tmp.length != 4) {
+ throw new IOException("insufficient data");
+ }
+ int value = (tmp[0] & 0xff) << 24;
+ value |= (tmp[1] & 0xff) << 16;
+ value |= (tmp[2] & 0xff) << 8;
+ value |= (tmp[3] & 0xff);
+ return value;
+ }
+
+ /**
+ * Read block of data from SSH-encoded buffer.
+ * (modeled after org.bouncycastle.crypto.util.SSHBuffer.readBlock())
+ * @param in data input stream
+ * @return byte array
+ * @throws IOException if IO error occurs reading input stream or inadequate
+ * bytes are available.
+ */
+ private static byte[] sshBufferReadBlock(ByteArrayInputStream in) throws IOException {
+ int len = sshBufferReadUInt32(in);
+ if (len <= 0 || len > in.available()) {
+ throw new IOException("insufficient data");
+ }
+ return in.readNBytes(len);
+ }
+
/**
* Complete the authentication process
* @param userMgr Ghidra server user manager
* @param subject unauthenticated user ID (must be used if name callback not provided/allowed)
* @param callbacks authentication callbacks
* @return authenticated user ID (may come from callbacks)
- * @throws LoginException
+ * @throws LoginException if authentication failure occurs
*/
public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks)
throws LoginException {
@@ -162,23 +210,41 @@ public class SSHAuthenticationModule {
}
try {
+ ByteArrayInputStream in = new ByteArrayInputStream(sigBytes);
+ String keyAlgorithm = Strings.fromByteArray(sshBufferReadBlock(in));
+ byte[] sig = sshBufferReadBlock(in);
+ if (in.available() != 0) {
+ throw new FailedLoginException("SSH Signature contained extra bytes");
+ }
- Object sshPublicKey = SSHKeyManager.getSSHPublicKey(sshPublicKeyFile);
+ CipherParameters cipherParams = SSHKeyManager.getSSHPublicKey(sshPublicKeyFile);
- if (sshPublicKey instanceof RSAPublicKey) {
- RSAPublicKey key = (RSAPublicKey) sshPublicKey;
- RSASignature rsaSignature = RSASHA1Verify.decodeSSHRSASignature(sigBytes);
- if (!RSASHA1Verify.verifySignature(token, rsaSignature, key)) {
+ if (cipherParams instanceof RSAKeyParameters) {
+ if (!"ssh-rsa".equals(keyAlgorithm)) {
+ throw new FailedLoginException("Invalid SSH RSA Signature");
+ }
+ RSADigestSigner signer = new RSADigestSigner(new SHA1Digest());
+ signer.init(false, cipherParams);
+ signer.update(token, 0, token.length);
+ if (!signer.verifySignature(sig)) {
throw new FailedLoginException("Incorrect signature");
}
}
- else if (sshPublicKey instanceof DSAPublicKey) {
- DSAPublicKey key = (DSAPublicKey) sshPublicKey;
- DSASignature dsaSignature = DSASHA1Verify.decodeSSHDSASignature(sigBytes);
- if (!DSASHA1Verify.verifySignature(token, dsaSignature, key)) {
+ else if (cipherParams instanceof DSAKeyParameters) {
+ if (!"ssh-dss".equals(keyAlgorithm)) {
+ throw new FailedLoginException("Invalid SSH DSA Signature");
+ }
+ DSADigestSigner signer = new DSADigestSigner(new DSASigner(), new SHA1Digest());
+ signer.init(false, cipherParams);
+ signer.update(token, 0, token.length);
+ if (!signer.verifySignature(sig)) {
throw new FailedLoginException("Incorrect signature");
}
}
+ else {
+ throw new FailedLoginException("Unsupported public key");
+ }
+
}
catch (LoginException e) {
throw e;
diff --git a/Ghidra/Framework/FileSystem/Module.manifest b/Ghidra/Framework/FileSystem/Module.manifest
index c0c2b1f0d5..e69de29bb2 100644
--- a/Ghidra/Framework/FileSystem/Module.manifest
+++ b/Ghidra/Framework/FileSystem/Module.manifest
@@ -1 +0,0 @@
-MODULE FILE LICENSE: lib/ganymed-ssh2-262.jar Christian Plattner
diff --git a/Ghidra/Framework/FileSystem/build.gradle b/Ghidra/Framework/FileSystem/build.gradle
index 56e98c9b31..9541f2b5a6 100644
--- a/Ghidra/Framework/FileSystem/build.gradle
+++ b/Ghidra/Framework/FileSystem/build.gradle
@@ -26,7 +26,5 @@ dependencies {
api project(':Generic')
api project(':DB')
api project(':Docking')
- api "ch.ethz.ganymed:ganymed-ssh2:262@jar"
-
}
diff --git a/Ghidra/Framework/FileSystem/certification.manifest b/Ghidra/Framework/FileSystem/certification.manifest
index 00b0b476dc..268c4261f3 100644
--- a/Ghidra/Framework/FileSystem/certification.manifest
+++ b/Ghidra/Framework/FileSystem/certification.manifest
@@ -1,5 +1,4 @@
##VERSION: 2.0
-##MODULE IP: Christian Plattner
Module.manifest||GHIDRA||||END|
src/main/java/ghidra/framework/client/package.html||GHIDRA||reviewed||END|
src/main/java/ghidra/framework/store/db/package.html||GHIDRA||reviewed||END|
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java
index 566db67d88..b1ad049b5b 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java
@@ -19,6 +19,7 @@ import java.awt.Component;
import java.io.*;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
+import java.security.InvalidKeyException;
import javax.security.auth.callback.*;
@@ -86,29 +87,23 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator {
ClientUtil.setClientAuthenticator(authenticator);
if (keystorePath != null) {
- File f = new File(keystorePath);
- if (!f.exists()) {
+ File keyfile = new File(keystorePath);
+ if (!keyfile.exists()) {
// If keystorePath file not found - try accessing as SSH key resource stream
// InputStream keyIn = ResourceManager.getResourceAsStream(keystorePath);
- InputStream keyIn = keystorePath.getClass().getResourceAsStream(keystorePath);
- if (keyIn != null) {
- try {
- sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyIn);
- Msg.info(HeadlessClientAuthenticator.class,
- "Loaded SSH key: " + keystorePath);
- return;
- }
- catch (Exception e) {
- Msg.error(HeadlessClientAuthenticator.class,
- "Failed to open keystore for SSH use: " + keystorePath, e);
- throw new IOException("Failed to parse keystore: " + keystorePath);
- }
- finally {
+ try (InputStream keyIn =
+ HeadlessClientAuthenticator.class.getResourceAsStream(keystorePath)) {
+ if (keyIn != null) {
try {
- keyIn.close();
+ sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyIn);
+ Msg.info(HeadlessClientAuthenticator.class,
+ "Loaded SSH key: " + keystorePath);
+ return;
}
- catch (IOException e) {
- // ignore
+ catch (Exception e) {
+ Msg.error(HeadlessClientAuthenticator.class,
+ "Failed to open keystore for SSH use: " + keystorePath, e);
+ throw new IOException("Failed to parse keystore: " + keystorePath);
}
}
}
@@ -116,24 +111,27 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator {
throw new FileNotFoundException("Keystore not found: " + keystorePath);
}
+ boolean success = false;
try {
- sshPrivateKey = SSHKeyManager.getSSHPrivateKey(new File(keystorePath));
+ sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyfile);
+ success = true;
Msg.info(HeadlessClientAuthenticator.class, "Loaded SSH key: " + keystorePath);
}
- catch (IOException e) {
- try {
- // try keystore as PKI keystore if failed as SSH keystore
- ApplicationKeyManagerFactory.setKeyStore(keystorePath, false);
- Msg.info(HeadlessClientAuthenticator.class, "Loaded PKI key: " + keystorePath);
- }
- catch (IOException e1) {
- Msg.error(HeadlessClientAuthenticator.class,
- "Failed to open keystore for PKI use: " + keystorePath, e1);
- Msg.error(HeadlessClientAuthenticator.class,
- "Failed to open keystore for SSH use: " + keystorePath, e);
- throw new IOException("Failed to parse keystore: " + keystorePath);
+ catch (InvalidKeyException e) { // keyfile is not a valid SSH provate key format
+ // does not appear to be an SSH private key - try PKI keystore parse
+ if (ApplicationKeyManagerFactory.setKeyStore(keystorePath, false)) {
+ success = true;
+ Msg.info(HeadlessClientAuthenticator.class,
+ "Loaded PKI keystore: " + keystorePath);
}
}
+ catch (IOException e) { // SSH key parse failure only
+ Msg.error(HeadlessClientAuthenticator.class,
+ "Failed to open keystore for SSH use: " + keystorePath, e);
+ }
+ if (!success) {
+ throw new IOException("Failed to parse keystore: " + keystorePath);
+ }
}
else {
sshPrivateKey = null;
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SSHSignatureCallback.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SSHSignatureCallback.java
index f21f6e0d64..2f6e1a6ace 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SSHSignatureCallback.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SSHSignatureCallback.java
@@ -15,14 +15,17 @@
*/
package ghidra.framework.remote;
-import java.io.IOException;
-import java.io.Serializable;
-import java.security.SecureRandom;
+import java.io.*;
import javax.security.auth.callback.Callback;
-import ch.ethz.ssh2.signature.*;
-import generic.random.SecureRandomFactory;
+import org.bouncycastle.crypto.CryptoException;
+import org.bouncycastle.crypto.DataLengthException;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+import org.bouncycastle.crypto.params.DSAKeyParameters;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+import org.bouncycastle.crypto.signers.*;
+import org.bouncycastle.util.Strings;
/**
* SSHSignatureCallback
provides a Callback implementation used
@@ -45,6 +48,7 @@ public class SSHSignatureCallback implements Callback, Serializable {
/**
* Construct callback with a random token to be signed by the client.
* @param token random bytes to be signed
+ * @param serverSignature server signature of token (using server PKI)
*/
public SSHSignatureCallback(byte[] token, byte[] serverSignature) {
this.token = token;
@@ -66,6 +70,7 @@ public class SSHSignatureCallback implements Callback, Serializable {
}
/**
+ * Get the server signature of token (using server PKI)
* @return the server's signature of the token bytes.
*/
public byte[] getServerSignature() {
@@ -80,28 +85,77 @@ public class SSHSignatureCallback implements Callback, Serializable {
}
/**
- * Sign this challenge with the specified SSH private key.
- * @param sshPrivateKey RSAPrivateKey or DSAPrivateKey
- * @throws IOException if signature generation failed
- * @see RSAPrivateKey
- * @see DSAPrivateKey
+ * Write UInt32 to an SSH-encoded buffer.
+ * (modeled after org.bouncycastle.crypto.util.SSHBuilder.u32(int))
+ * @param value integer value
+ * @param out data output stream
*/
- public void sign(Object sshPrivateKey) throws IOException {
- if (sshPrivateKey instanceof RSAPrivateKey) {
- RSAPrivateKey key = (RSAPrivateKey) sshPrivateKey;
- // TODO: verify correct key by using accepted public key fingerprint
- RSASignature rsaSignature = RSASHA1Verify.generateSignature(token, key);
- signature = RSASHA1Verify.encodeSSHRSASignature(rsaSignature);
+ private static void sshBuilderWriteUInt32(int value, ByteArrayOutputStream out) {
+ byte[] tmp = new byte[4];
+ tmp[0] = (byte) ((value >>> 24) & 0xff);
+ tmp[1] = (byte) ((value >>> 16) & 0xff);
+ tmp[2] = (byte) ((value >>> 8) & 0xff);
+ tmp[3] = (byte) (value & 0xff);
+ out.writeBytes(tmp);
+ }
+
+ /**
+ * Write byte array to an SSH-encoded buffer.
+ * (modeled after org.bouncycastle.crypto.util.SSHBuilder.writeBlock(byte[])
+ * @param value byte array
+ * @param out data output stream
+ */
+ private static void sshBuilderWriteBlock(byte[] value, ByteArrayOutputStream out) {
+ sshBuilderWriteUInt32(value.length, out);
+ out.writeBytes(value);
+ }
+
+ /**
+ * Write string to an SSH-encoded buffer.
+ * (modeled after org.bouncycastle.crypto.util.SSHBuilder.writeString(String)
+ * @param str string data
+ * @param out data output stream
+ */
+ private static void sshBuilderWriteString(String str, ByteArrayOutputStream out) {
+ sshBuilderWriteBlock(Strings.toByteArray(str), out);
+ }
+
+ /**
+ * Sign this challenge with the specified SSH private key.
+ * @param privateKeyParameters SSH private key parameters
+ * ({@link RSAKeyParameters} or {@link RSAKeyParameters})
+ * @throws IOException if signature generation failed
+ */
+ public void sign(Object privateKeyParameters) throws IOException {
+ try {
+ // NOTE: Signature is formatted consistent with legacy implementation
+ // for backward compatibility
+ if (privateKeyParameters instanceof RSAKeyParameters) {
+ RSAKeyParameters cipherParams = (RSAKeyParameters) privateKeyParameters;
+ RSADigestSigner signer = new RSADigestSigner(new SHA1Digest());
+ signer.init(true, cipherParams);
+ signer.update(token, 0, token.length);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ sshBuilderWriteString("ssh-rsa", out);
+ sshBuilderWriteBlock(signer.generateSignature(), out);
+ signature = out.toByteArray();
+ }
+ else if (privateKeyParameters instanceof DSAKeyParameters) {
+ DSAKeyParameters cipherParams = (DSAKeyParameters) privateKeyParameters;
+ DSADigestSigner signer = new DSADigestSigner(new DSASigner(), new SHA1Digest());
+ signer.init(true, cipherParams);
+ signer.update(token, 0, token.length);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ sshBuilderWriteString("ssh-dss", out);
+ sshBuilderWriteBlock(signer.generateSignature(), out);
+ signature = out.toByteArray();
+ }
+ else {
+ throw new IllegalArgumentException("Unsupported SSH private key");
+ }
}
- else if (sshPrivateKey instanceof DSAPrivateKey) {
- DSAPrivateKey key = (DSAPrivateKey) sshPrivateKey;
- // TODO: verify correct key by using accepted public key fingerprint
- SecureRandom random = SecureRandomFactory.getSecureRandom();
- DSASignature dsaSignature = DSASHA1Verify.generateSignature(token, key, random);
- signature = DSASHA1Verify.encodeSSHDSASignature(dsaSignature);
- }
- else {
- throw new IllegalArgumentException("Unsupported SSH private key");
+ catch (DataLengthException | CryptoException e) {
+ throw new IOException("Cannot generate SSH signature: " + e.getMessage(), e);
}
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/security/SSHKeyManager.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/security/SSHKeyManager.java
index aeedb11a02..72363dc477 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/security/SSHKeyManager.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/security/SSHKeyManager.java
@@ -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.
@@ -16,26 +15,30 @@
*/
package ghidra.framework.remote.security;
-import ghidra.security.KeyStorePasswordProvider;
-
import java.io.*;
+import java.security.InvalidKeyException;
+import java.security.Security;
+import java.util.Arrays;
-import ch.ethz.ssh2.crypto.Base64;
-import ch.ethz.ssh2.crypto.PEMDecoder;
-import ch.ethz.ssh2.signature.*;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.crypto.CipherParameters;
+import org.bouncycastle.crypto.params.DSAKeyParameters;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
+import org.bouncycastle.crypto.util.PrivateKeyFactory;
+import org.bouncycastle.openssl.*;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.bouncycastle.util.encoders.Base64;
+
+import ghidra.security.KeyStorePasswordProvider;
+import ghidra.util.Msg;
public class SSHKeyManager {
-// private static final String DEFAULT_KEYSTORE_PATH =
-// System.getProperty("user.home") + File.separator + ".ssh/id_rsa";
-//
-// /**
-// * Preference name for the SSH key file paths
-// */
-// private static final String SSH_KEYSTORE_PROPERTY = "ghidra.sshKeyFile";
-
- // The public key file is derived by adding this extension to the key store filename
- //private static final String SSH_PUBLIC_KEY_EXT1 = ".pub";
+ static {
+ // For JcaPEMKeyConverter().setProvider("BC")
+ Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+ }
private static KeyStorePasswordProvider passwordProvider;
@@ -45,148 +48,143 @@ public class SSHKeyManager {
/**
* Set PKI protected keystore password provider
- * @param provider
+ * @param provider key store password provider
*/
public static synchronized void setProtectedKeyStorePasswordProvider(
KeyStorePasswordProvider provider) {
passwordProvider = provider;
}
-// /**
-// * Return the SSH private key for the current user. The default ~/.ssh/id_rsa file
-// * will be used unless the System property ghidra.sshKeyFile has been set.
-// * If the corresponding key file is encrypted the currently installed password
-// * provider will be used to obtain the decrypt password.
-// * @return RSAPrivateKey or DSAPrivateKey
-// * @throws FileNotFoundException key file not found
-// * @throws IOException if key file not found or key parse failed
-// * @see RSAPrivateKey
-// * @see DSAPrivateKey
-// */
-// public static Object getUsersSSHPrivateKey() throws IOException {
-//
-// String privateKeyStorePath = System.getProperty(SSH_KEYSTORE_PROPERTY);
-// if (privateKeyStorePath == null) {
-// privateKeyStorePath = DEFAULT_KEYSTORE_PATH;
-// }
-//
-// return getSSHPrivateKey(new File(privateKeyStorePath));
-// }
-
/**
* Return the SSH private key corresponding to the specified key file.
* If the specified key file is encrypted the currently installed password
* provider will be used to obtain the decrypt password.
- * @param sshPrivateKeyFile
- * @return RSAPrivateKey or DSAPrivateKey
+ * @param sshPrivateKeyFile private ssh key file
+ * @return private key cipher parameters ({@link RSAKeyParameters} or {@link DSAKeyParameters})
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
- * @see RSAPrivateKey
- * @see DSAPrivateKey
+ * @throws InvalidKeyException if key is not an SSH private key (i.e., PEM format)
*/
- public static Object getSSHPrivateKey(File sshPrivateKeyFile) throws IOException {
+ public static CipherParameters getSSHPrivateKey(File sshPrivateKeyFile)
+ throws InvalidKeyException, IOException {
if (!sshPrivateKeyFile.isFile()) {
throw new FileNotFoundException("SSH private key file not found: " + sshPrivateKeyFile);
}
- InputStream keyIn = new FileInputStream(sshPrivateKeyFile);
- try {
+ try (InputStream keyIn = new FileInputStream(sshPrivateKeyFile)) {
return getSSHPrivateKey(keyIn, sshPrivateKeyFile.getAbsolutePath());
}
- finally {
- try {
- keyIn.close();
- }
- catch (IOException e) {
- }
- }
}
/**
* Return the SSH private key corresponding to the specified key input stream.
* If the specified key is encrypted the currently installed password
* provider will be used to obtain the decrypt password.
- * @param sshPrivateKeyIn
- * @return RSAPrivateKey or DSAPrivateKey
+ * @param sshPrivateKeyIn private ssh key resource input stream
+ * @return private key cipher parameters ({@link RSAKeyParameters} or {@link DSAKeyParameters})
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
- * @see RSAPrivateKey
- * @see DSAPrivateKey
+ * @throws InvalidKeyException if key is not an SSH private key (i.e., PEM format)
*/
- public static Object getSSHPrivateKey(InputStream sshPrivateKeyIn) throws IOException {
+ public static CipherParameters getSSHPrivateKey(InputStream sshPrivateKeyIn)
+ throws InvalidKeyException, IOException {
return getSSHPrivateKey(sshPrivateKeyIn, "Protected SSH Key");
}
- private static Object getSSHPrivateKey(InputStream sshPrivateKeyIn, String srcName)
- throws IOException {
+ private static CipherParameters getSSHPrivateKey(InputStream sshPrivateKeyIn, String srcName)
+ throws InvalidKeyException, IOException {
- boolean isEncrypted = false;
StringBuffer keyBuf = new StringBuffer();
- BufferedReader r = new BufferedReader(new InputStreamReader(sshPrivateKeyIn));
- String line;
- while ((line = r.readLine()) != null) {
- if (line.startsWith("Proc-Type:")) {
- isEncrypted = (line.indexOf("ENCRYPTED") > 0);
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(sshPrivateKeyIn))) {
+ boolean checkKeyFormat = true;
+ String line;
+ while ((line = r.readLine()) != null) {
+ if (checkKeyFormat) {
+ if (!line.startsWith("-----BEGIN ") || line.indexOf(" KEY-----") < 0) {
+ throw new InvalidKeyException("Invalid SSH Private Key");
+ }
+ if (!line.startsWith("-----BEGIN RSA PRIVATE KEY-----") &&
+ !line.startsWith("-----BEGIN DSA PRIVATE KEY-----")) {
+ Msg.error(SSHKeyManager.class,
+ "Unsupported SSH Key Format (see svrREADME.html)");
+ throw new IOException("Unsupported SSH Private Key");
+ }
+ checkKeyFormat = false;
+ }
+ if (keyBuf.length() != 0) {
+ keyBuf.append('\n');
+ }
+ keyBuf.append(line);
}
- keyBuf.append(line);
- keyBuf.append('\n');
- }
- r.close();
-
- String password = null;
- if (isEncrypted) {
- char[] pwd = passwordProvider.getKeyStorePassword(srcName, false);
- if (pwd == null) {
- throw new IOException("Password required to open SSH private keystore");
- }
- // Don't like using String for password - but API doesn't give us a choice
- password = new String(pwd);
}
- return PEMDecoder.decode(keyBuf.toString().toCharArray(), password);
+ char[] password = null;
+ try (Reader r = new StringReader(keyBuf.toString())) {
+ PEMParser pemParser = new PEMParser(r);
+ Object object = pemParser.readObject();
+
+ PrivateKeyInfo privateKeyInfo;
+ if (object instanceof PEMEncryptedKeyPair) {
+
+ password = passwordProvider.getKeyStorePassword(srcName, false);
+ if (password == null) {
+ throw new IOException("Password required to open SSH private keystore");
+ }
+
+ // Encrypted key - we will use provided password
+ PEMEncryptedKeyPair ckp = (PEMEncryptedKeyPair) object;
+ PEMDecryptorProvider decProv =
+ new JcePEMDecryptorProviderBuilder().build(password);
+ privateKeyInfo = ckp.decryptKeyPair(decProv).getPrivateKeyInfo();
+ }
+ else {
+ // Unencrypted key - no password needed
+ PEMKeyPair ukp = (PEMKeyPair) object;
+ privateKeyInfo = ukp.getPrivateKeyInfo();
+ }
+ return PrivateKeyFactory.createKey(privateKeyInfo);
+ }
+ finally {
+ if (password != null) {
+ Arrays.fill(password, (char) 0);
+ }
+ }
}
/**
* Attempt to instantiate an SSH public key from the specified file
* which contains a single public key.
- * @param sshPublicKeyFile
- * @return RSAPublicKey or DSAPublicKey
+ * @param sshPublicKeyFile public ssh key file
+ * @return public key cipher parameters {@link RSAKeyParameters} or {@link DSAKeyParameters}
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
- * @see RSAPublicKey
- * @see DSAPublicKey
*/
- public static Object getSSHPublicKey(File sshPublicKeyFile) throws IOException {
+ public static CipherParameters getSSHPublicKey(File sshPublicKeyFile) throws IOException {
- BufferedReader r = new BufferedReader(new FileReader(sshPublicKeyFile));
String keyLine = null;
- String line;
- while ((line = r.readLine()) != null) {
- if (!line.startsWith("ssh-")) {
- continue;
+ try (BufferedReader r = new BufferedReader(new FileReader(sshPublicKeyFile))) {
+ String line;
+ while ((line = r.readLine()) != null) {
+ if (!line.startsWith("ssh-")) {
+ continue;
+ }
+ keyLine = line;
+ break;
}
- keyLine = line;
- break;
}
- r.close();
if (keyLine != null) {
- String[] pieces = keyLine.split(" ");
- if (pieces.length >= 2) {
- byte[] pubkeyBytes = Base64.decode(pieces[1].toCharArray());
- if ("ssh-rsa".equals(pieces[0])) {
- return RSASHA1Verify.decodeSSHRSAPublicKey(pubkeyBytes);
- }
- else if ("ssh-dsa".equals(pieces[0])) {
- return DSASHA1Verify.decodeSSHDSAPublicKey(pubkeyBytes);
- }
+ String[] part = keyLine.split("\\s+");
+ if (part.length >= 2 && part[0].startsWith("ssh-")) {
+ byte[] pubkeyBytes = Base64.decode(part[1]);
+ return OpenSSHPublicKeyUtil.parsePublicKey(pubkeyBytes);
}
}
throw new IOException(
- "Invalid SSH public key file, valid ssh-rsa or ssh-dsa entry not found: " +
+ "Invalid SSH public key file, supported SSH public key not found: " +
sshPublicKeyFile);
}
diff --git a/Ghidra/Framework/Generic/certification.manifest b/Ghidra/Framework/Generic/certification.manifest
index 8c9e646799..0e568aba7c 100644
--- a/Ghidra/Framework/Generic/certification.manifest
+++ b/Ghidra/Framework/Generic/certification.manifest
@@ -2,7 +2,6 @@
##MODULE IP: Apache License 2.0
##MODULE IP: Bouncy Castle License
##MODULE IP: BSD
-##MODULE IP: Christian Plattner
##MODULE IP: Crystal Clear Icons - LGPL 2.1
##MODULE IP: FAMFAMFAM Icons - CC 2.5
##MODULE IP: JDOM License
diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java b/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java
index fd249ebaaa..3975fab8db 100644
--- a/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java
@@ -127,17 +127,16 @@ public class ApplicationKeyManagerFactory {
* This change will take immediate effect for the current executing application,
* however, it may still be superseded by a system property setting when running
* the application in the future. See {@link #getKeyStore()}.
- * @param path keystore file path
+ * @param path keystore file path or null to clear current key store and preference.
* @param savePreference if true will be saved as user preference
- * @throws IOException if file or certificate error occurs
+ * @return true if successful else false if error occured (see log).
*/
- public static synchronized void setKeyStore(String path, boolean savePreference)
- throws IOException {
+ public static synchronized boolean setKeyStore(String path, boolean savePreference) {
if (System.getProperty(KEYSTORE_PATH_PROPERTY) != null) {
Msg.showError(ApplicationKeyManagerFactory.class, null, "Set KeyStore Failed",
- "KeyStore was set via system property and can not be changed");
- return;
+ "PKI KeyStore was set via system property and can not be changed");
+ return false;
}
path = prunePath(path);
@@ -149,9 +148,11 @@ public class ApplicationKeyManagerFactory {
Preferences.setProperty(KEYSTORE_PATH_PROPERTY, path);
Preferences.store();
}
+ return keyInitialized;
}
catch (CancelledException e) {
// ignore - keystore left unchanged
+ return false;
}
}
@@ -509,7 +510,9 @@ public class ApplicationKeyManagerFactory {
* has been set, a self-signed certificate will be generated. If nothing has been set, the
* wrappedKeyManager will remain null and false will be returned. If an error occurs it
* will be logged and key managers will remain uninitialized.
- * @return true if key manager initialized successfully or was previously initialized.
+ * @return true if key manager initialized successfully or was previously initialized, else
+ * false if keystore path has not been set and default identity for self-signed certificate
+ * has not be established (see {@link ApplicationKeyManagerFactory#setDefaultIdentity(X500Principal)}).
* @throws CancelledException user cancelled keystore password entry request
*/
private synchronized boolean init() throws CancelledException {
@@ -527,7 +530,9 @@ public class ApplicationKeyManagerFactory {
* wrappedKeyManager will remain null and false will be returned. If an error occurs it
* will be logged and key managers will remain uninitialized.
* @param newKeystorePath specifies the keystore to be opened or null for no keystore
- * @return true if key manager initialized successfully or was previously initialized
+ * @return true if key manager initialized successfully or was previously initialized, else
+ * false if new keystore path was not specified and default identity for self-signed certificate
+ * has not be established (see {@link ApplicationKeyManagerFactory#setDefaultIdentity(X500Principal)}).
* @throws CancelledException user cancelled keystore password entry request
*/
private synchronized boolean init(String newKeystorePath) throws CancelledException {
@@ -576,25 +581,25 @@ public class ApplicationKeyManagerFactory {
isSelfSigned = false;
if (keyManagers.length == 0) {
- Msg.showError(this, null, "Keystore Failure",
- "Failed to create key manager: failed to process keystore (no keys processed)");
+ Msg.showError(this, null, "PKI Keystore Failure",
+ "Failed to create PKI key manager: failed to process keystore (no keys processed)");
}
else if (keyManagers.length == 1) {
- Msg.showError(this, null, "Keystore Failure",
- "Failed to create key manager: failed to process keystore (expected X.509)");
+ Msg.showError(this, null, "PKI Keystore Failure",
+ "Failed to create PKI key manager: failed to process keystore (expected X.509)");
}
else {
// Unexpected condition
- Msg.showError(this, null, "Keystore Failure",
- "Failed to create key manager: unsupported keystore produced multiple KeyManagers");
+ Msg.showError(this, null, "PKI Keystore Failure",
+ "Failed to create PKI key manager: unsupported keystore produced multiple KeyManagers");
}
}
catch (CancelledException e) {
throw e;
}
catch (Exception e) {
- Msg.showError(this, null, "Keystore Failure",
- "Failed to create key manager: " + e.getMessage(), e);
+ Msg.showError(this, null, "PKI Keystore Failure",
+ "Failed to create PKI key manager: " + e.getMessage(), e);
}
finally {
if (keystoreData != null) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java
index 18cd6d0c0f..2d56336d3e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java
@@ -16,7 +16,6 @@
package ghidra.framework.main;
import java.io.File;
-import java.io.IOException;
import docking.ActionContext;
import docking.action.DockingAction;
@@ -26,7 +25,6 @@ import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.HelpLocation;
-import ghidra.util.Msg;
/**
* Helper class to manage the actions on the Edit menu.
@@ -121,14 +119,8 @@ class EditActionManager {
return;
}
- try {
- ApplicationKeyManagerFactory.setKeyStore(null, true);
- clearCertPathAction.setEnabled(false);
- }
- catch (IOException e) {
- Msg.error(this,
- "Error occurred while clearing PKI certificate setting: " + e.getMessage());
- }
+ ApplicationKeyManagerFactory.setKeyStore(null, true);
+ clearCertPathAction.setEnabled(false);
}
private void editCertPath() {
@@ -167,16 +159,9 @@ class EditActionManager {
if (file == null) {
return; // cancelled
}
- try {
- ApplicationKeyManagerFactory.setKeyStore(file.getAbsolutePath(), true);
- clearCertPathAction.setEnabled(true);
- validInput = true;
- }
- catch (IOException e) {
- Msg.showError(this, tool.getToolFrame(), "Certificate Failure",
- "Failed to initialize key manager.\n" + e.getMessage(), e);
- file = null;
- }
+ ApplicationKeyManagerFactory.setKeyStore(file.getAbsolutePath(), true);
+ clearCertPathAction.setEnabled(true);
+ validInput = true;
}
}
diff --git a/Ghidra/RuntimeScripts/Common/server/svrREADME.html b/Ghidra/RuntimeScripts/Common/server/svrREADME.html
index cc5a1235cc..24cc6d691f 100644
--- a/Ghidra/RuntimeScripts/Common/server/svrREADME.html
+++ b/Ghidra/RuntimeScripts/Common/server/svrREADME.html
@@ -320,6 +320,17 @@ eliminate SSH based authentication for the corresponding user. When creating th
owner with full access and any SSH public keys readable by the process owner. Changes to the SSH
public key files may be made without restarting the Ghidra Server.
Each user may generate a suitable SSH key pair with the
+ ssh-keygen -m pem -t rsa -b 2048 ++
NOTE: Ghidra Server authentication does not currently support the OPENSSH key format which may be the default
+