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 command issued from a +shell prompt. A PEM formatted RSA key-pair should be generated using the following command options: +

+
+   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 +ssh-keygen format (-m option) on some systems such as Ubuntu. +In addition, other key types (-t option) such as ecdsa and ed25519 +are not currently supported. +

(Back to Top)
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/SSHKeyUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/SSHKeyUtil.java index 4364775992..6bdd663bab 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/SSHKeyUtil.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/SSHKeyUtil.java @@ -16,21 +16,22 @@ package ghidra.server.remote; import java.io.ByteArrayOutputStream; +import java.math.BigInteger; import java.security.*; import java.security.interfaces.*; import java.util.Base64; -import ch.ethz.ssh2.packets.TypesWriter; +import org.bouncycastle.util.Strings; public class SSHKeyUtil { /** - * Generate private/public SSH keys for test purposes using RSA algorithm. + * Generate private/public SSH RSA keys for test purposes using RSA algorithm. * @return kay pair array suitable for writing to SSH private and public - * key files ([0] corresponds to private key, [1] corresponds to public key) - * @throws NoSuchAlgorithmException + * key files ([0] corresponds to private key PEM file, [1] corresponds to public key file) + * @throws NoSuchAlgorithmException failed to instantiate RSA key pair generator */ - public static String[] generateSSHKeys() throws NoSuchAlgorithmException { + public static String[] generateSSHRSAKeys() throws NoSuchAlgorithmException { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); @@ -84,19 +85,64 @@ public class SSHKeyUtil { out.writeBytes(data); } - private static String getRSAPublicKey(KeyPair rsaKeyPair) { - String keyAlgorithm = "ssh-rsa"; - RSAPublicKey rsaPublicKey = (RSAPublicKey) rsaKeyPair.getPublic(); - TypesWriter w = new TypesWriter(); - w.writeString(keyAlgorithm); - w.writeMPInt(rsaPublicKey.getPublicExponent()); - w.writeMPInt(rsaPublicKey.getModulus()); + /** + * 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 + */ + 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 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) { + byte[] data = Strings.toByteArray(str); + sshBuilderWriteUInt32(data.length, out); + out.writeBytes(data); + } + + /** + * Generate SSH RSA public key file content + * @param rsaKeyPair SSH public/private key pair + * @return SSH public key file content string + */ + private static String getRSAPublicKey(KeyPair rsaKeyPair) { + + RSAPublicKey rsaPublicKey = (RSAPublicKey) rsaKeyPair.getPublic(); + String keyAlgorithm = "ssh-rsa"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + sshBuilderWriteString(keyAlgorithm, out); + BigInteger e = rsaPublicKey.getPublicExponent(); + byte[] data = e.toByteArray(); + sshBuilderWriteUInt32(data.length, out); + out.writeBytes(data); + BigInteger m = rsaPublicKey.getModulus(); + data = m.toByteArray(); + sshBuilderWriteUInt32(data.length, out); + out.writeBytes(data); + byte[] bytesOut = out.toByteArray(); - byte[] bytesOut = w.getBytes(); String publicKeyEncoded = new String(Base64.getEncoder().encodeToString(bytesOut)); return keyAlgorithm + " " + publicKeyEncoded + " test\n"; } + /** + * Generate SSH RSA private key file content in PEM format + * @param rsaKeyPair SSH public/private key pair + * @return SSH private key file content string in PEM format + */ private static String getRSAPrivateKey(KeyPair rsaKeyPair) { RSAPrivateKey privateKey = (RSAPrivateKey) rsaKeyPair.getPrivate(); RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) privateKey; diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java index 3d863fb830..9b2dcdd337 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java @@ -827,7 +827,7 @@ public class ServerTestUtil { System.arraycopy(users, 0, userArray, 1, users.length); createUsers(dirPath, userArray); - String keys[] = SSHKeyUtil.generateSSHKeys(); + String keys[] = SSHKeyUtil.generateSSHRSAKeys(); addSSHKeys(dirPath, keys[0], "test.key", keys[1], "test.pub"); LocalFileSystem repoFilesystem = createRepository(dirPath, "Test", ADMIN_USER + "=ADMIN", diff --git a/licenses/Christian_Plattner.txt b/licenses/Christian_Plattner.txt deleted file mode 100644 index ea00063481..0000000000 --- a/licenses/Christian_Plattner.txt +++ /dev/null @@ -1,87 +0,0 @@ -Copyright (c) 2006 - 2010 Christian Plattner. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -a.) Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -b.) Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -c.) Neither the name of Christian Plattner nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - -This software includes work that was released under the following license: - -Copyright (c) 2005 - 2006 Swiss Federal Institute of Technology (ETH Zurich), - Department of Computer Science (http://www.inf.ethz.ch), - Christian Plattner. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -a.) Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -b.) Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -c.) Neither the name of ETH Zurich nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - -The Java implementations of the AES, Blowfish and 3DES ciphers have been -taken (and slightly modified) from the cryptography package released by -"The Legion Of The Bouncy Castle". - -Their license states the following: - -Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle -(http://www.bouncycastle.org) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/licenses/certification.manifest b/licenses/certification.manifest index c9b0b47d72..5e1b93de55 100644 --- a/licenses/certification.manifest +++ b/licenses/certification.manifest @@ -3,7 +3,6 @@ Apache_License_2.0.txt||LICENSE||||END| Apache_License_2.0_with_LLVM_Exceptions.txt||LICENSE||||END| BSD.txt||LICENSE||||END| Bouncy_Castle_License.txt||LICENSE||||END| -Christian_Plattner.txt||LICENSE||||END| Creative_Commons_Attribution_2.5.html||LICENSE||||END| Crystal_Clear_Icons_-_LGPL_2.1.txt||LICENSE||||END| FAMFAMFAM_Icons_-_CC_2.5.txt||LICENSE||||END|