GP-1769 improved Ghidra Server SSH key use and error handling. Replaced use of ganymed-ssh2 library with Bouncy Castle library use.

This commit is contained in:
ghidra1 2022-03-03 17:26:09 -05:00
parent 1a1d06b749
commit 8c209ce76e
15 changed files with 384 additions and 314 deletions

View file

@ -1 +0,0 @@
MODULE FILE LICENSE: lib/ganymed-ssh2-262.jar Christian Plattner

View file

@ -26,7 +26,5 @@ dependencies {
api project(':Generic')
api project(':DB')
api project(':Docking')
api "ch.ethz.ganymed:ganymed-ssh2:262@jar"
}

View file

@ -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|

View file

@ -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;

View file

@ -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;
/**
* <code>SSHSignatureCallback</code> 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);
}
}

View file

@ -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 <i>ghidra.sshKeyFile</i> 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);
}