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

@ -15,7 +15,7 @@
*/ */
package ghidra.server.security; package ghidra.server.security;
import java.io.File; import java.io.*;
import java.util.*; import java.util.*;
import javax.security.auth.Subject; 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.FailedLoginException;
import javax.security.auth.login.LoginException; 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.GhidraPrincipal;
import ghidra.framework.remote.SSHSignatureCallback; import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.framework.remote.security.SSHKeyManager; import ghidra.framework.remote.security.SSHKeyManager;
import ghidra.net.*; import ghidra.net.*;
import ghidra.server.UserManager; import ghidra.server.UserManager;
/**
* <code>SSHAuthenticationModule</code> 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 { public class SSHAuthenticationModule {
private static final long MAX_TOKEN_TIME = 10000; private static final long MAX_TOKEN_TIME = 10000;
@ -86,13 +98,49 @@ public class SSHAuthenticationModule {
return false; 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 * Complete the authentication process
* @param userMgr Ghidra server user manager * @param userMgr Ghidra server user manager
* @param subject unauthenticated user ID (must be used if name callback not provided/allowed) * @param subject unauthenticated user ID (must be used if name callback not provided/allowed)
* @param callbacks authentication callbacks * @param callbacks authentication callbacks
* @return authenticated user ID (may come from 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) public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks)
throws LoginException { throws LoginException {
@ -162,23 +210,41 @@ public class SSHAuthenticationModule {
} }
try { 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) { if (cipherParams instanceof RSAKeyParameters) {
RSAPublicKey key = (RSAPublicKey) sshPublicKey; if (!"ssh-rsa".equals(keyAlgorithm)) {
RSASignature rsaSignature = RSASHA1Verify.decodeSSHRSASignature(sigBytes); throw new FailedLoginException("Invalid SSH RSA Signature");
if (!RSASHA1Verify.verifySignature(token, rsaSignature, key)) { }
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"); throw new FailedLoginException("Incorrect signature");
} }
} }
else if (sshPublicKey instanceof DSAPublicKey) { else if (cipherParams instanceof DSAKeyParameters) {
DSAPublicKey key = (DSAPublicKey) sshPublicKey; if (!"ssh-dss".equals(keyAlgorithm)) {
DSASignature dsaSignature = DSASHA1Verify.decodeSSHDSASignature(sigBytes); throw new FailedLoginException("Invalid SSH DSA Signature");
if (!DSASHA1Verify.verifySignature(token, dsaSignature, key)) { }
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"); throw new FailedLoginException("Incorrect signature");
} }
} }
else {
throw new FailedLoginException("Unsupported public key");
}
} }
catch (LoginException e) { catch (LoginException e) {
throw e; throw e;

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(':Generic')
api project(':DB') api project(':DB')
api project(':Docking') api project(':Docking')
api "ch.ethz.ganymed:ganymed-ssh2:262@jar"
} }

View file

@ -1,5 +1,4 @@
##VERSION: 2.0 ##VERSION: 2.0
##MODULE IP: Christian Plattner
Module.manifest||GHIDRA||||END| Module.manifest||GHIDRA||||END|
src/main/java/ghidra/framework/client/package.html||GHIDRA||reviewed||END| src/main/java/ghidra/framework/client/package.html||GHIDRA||reviewed||END|
src/main/java/ghidra/framework/store/db/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.io.*;
import java.net.Authenticator; import java.net.Authenticator;
import java.net.PasswordAuthentication; import java.net.PasswordAuthentication;
import java.security.InvalidKeyException;
import javax.security.auth.callback.*; import javax.security.auth.callback.*;
@ -86,29 +87,23 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator {
ClientUtil.setClientAuthenticator(authenticator); ClientUtil.setClientAuthenticator(authenticator);
if (keystorePath != null) { if (keystorePath != null) {
File f = new File(keystorePath); File keyfile = new File(keystorePath);
if (!f.exists()) { if (!keyfile.exists()) {
// If keystorePath file not found - try accessing as SSH key resource stream // If keystorePath file not found - try accessing as SSH key resource stream
// InputStream keyIn = ResourceManager.getResourceAsStream(keystorePath); // InputStream keyIn = ResourceManager.getResourceAsStream(keystorePath);
InputStream keyIn = keystorePath.getClass().getResourceAsStream(keystorePath); try (InputStream keyIn =
if (keyIn != null) { HeadlessClientAuthenticator.class.getResourceAsStream(keystorePath)) {
try { if (keyIn != null) {
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 { try {
keyIn.close(); sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyIn);
Msg.info(HeadlessClientAuthenticator.class,
"Loaded SSH key: " + keystorePath);
return;
} }
catch (IOException e) { catch (Exception e) {
// ignore 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); throw new FileNotFoundException("Keystore not found: " + keystorePath);
} }
boolean success = false;
try { try {
sshPrivateKey = SSHKeyManager.getSSHPrivateKey(new File(keystorePath)); sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyfile);
success = true;
Msg.info(HeadlessClientAuthenticator.class, "Loaded SSH key: " + keystorePath); Msg.info(HeadlessClientAuthenticator.class, "Loaded SSH key: " + keystorePath);
} }
catch (IOException e) { catch (InvalidKeyException e) { // keyfile is not a valid SSH provate key format
try { // does not appear to be an SSH private key - try PKI keystore parse
// try keystore as PKI keystore if failed as SSH keystore if (ApplicationKeyManagerFactory.setKeyStore(keystorePath, false)) {
ApplicationKeyManagerFactory.setKeyStore(keystorePath, false); success = true;
Msg.info(HeadlessClientAuthenticator.class, "Loaded PKI key: " + keystorePath); Msg.info(HeadlessClientAuthenticator.class,
} "Loaded PKI keystore: " + 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 (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 { else {
sshPrivateKey = null; sshPrivateKey = null;

View file

@ -15,14 +15,17 @@
*/ */
package ghidra.framework.remote; package ghidra.framework.remote;
import java.io.IOException; import java.io.*;
import java.io.Serializable;
import java.security.SecureRandom;
import javax.security.auth.callback.Callback; import javax.security.auth.callback.Callback;
import ch.ethz.ssh2.signature.*; import org.bouncycastle.crypto.CryptoException;
import generic.random.SecureRandomFactory; 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 * <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. * Construct callback with a random token to be signed by the client.
* @param token random bytes to be signed * @param token random bytes to be signed
* @param serverSignature server signature of token (using server PKI)
*/ */
public SSHSignatureCallback(byte[] token, byte[] serverSignature) { public SSHSignatureCallback(byte[] token, byte[] serverSignature) {
this.token = token; 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. * @return the server's signature of the token bytes.
*/ */
public byte[] getServerSignature() { public byte[] getServerSignature() {
@ -80,28 +85,77 @@ public class SSHSignatureCallback implements Callback, Serializable {
} }
/** /**
* Sign this challenge with the specified SSH private key. * Write UInt32 to an SSH-encoded buffer.
* @param sshPrivateKey RSAPrivateKey or DSAPrivateKey * (modeled after org.bouncycastle.crypto.util.SSHBuilder.u32(int))
* @throws IOException if signature generation failed * @param value integer value
* @see RSAPrivateKey * @param out data output stream
* @see DSAPrivateKey
*/ */
public void sign(Object sshPrivateKey) throws IOException { private static void sshBuilderWriteUInt32(int value, ByteArrayOutputStream out) {
if (sshPrivateKey instanceof RSAPrivateKey) { byte[] tmp = new byte[4];
RSAPrivateKey key = (RSAPrivateKey) sshPrivateKey; tmp[0] = (byte) ((value >>> 24) & 0xff);
// TODO: verify correct key by using accepted public key fingerprint tmp[1] = (byte) ((value >>> 16) & 0xff);
RSASignature rsaSignature = RSASHA1Verify.generateSignature(token, key); tmp[2] = (byte) ((value >>> 8) & 0xff);
signature = RSASHA1Verify.encodeSSHRSASignature(rsaSignature); 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) { catch (DataLengthException | CryptoException e) {
DSAPrivateKey key = (DSAPrivateKey) sshPrivateKey; throw new IOException("Cannot generate SSH signature: " + e.getMessage(), e);
// 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");
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,26 +15,30 @@
*/ */
package ghidra.framework.remote.security; package ghidra.framework.remote.security;
import ghidra.security.KeyStorePasswordProvider;
import java.io.*; import java.io.*;
import java.security.InvalidKeyException;
import java.security.Security;
import java.util.Arrays;
import ch.ethz.ssh2.crypto.Base64; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import ch.ethz.ssh2.crypto.PEMDecoder; import org.bouncycastle.crypto.CipherParameters;
import ch.ethz.ssh2.signature.*; 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 { public class SSHKeyManager {
// private static final String DEFAULT_KEYSTORE_PATH = static {
// System.getProperty("user.home") + File.separator + ".ssh/id_rsa"; // For JcaPEMKeyConverter().setProvider("BC")
// Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
// /** }
// * 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";
private static KeyStorePasswordProvider passwordProvider; private static KeyStorePasswordProvider passwordProvider;
@ -45,148 +48,143 @@ public class SSHKeyManager {
/** /**
* Set PKI protected keystore password provider * Set PKI protected keystore password provider
* @param provider * @param provider key store password provider
*/ */
public static synchronized void setProtectedKeyStorePasswordProvider( public static synchronized void setProtectedKeyStorePasswordProvider(
KeyStorePasswordProvider provider) { KeyStorePasswordProvider provider) {
passwordProvider = 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. * Return the SSH private key corresponding to the specified key file.
* If the specified key file is encrypted the currently installed password * If the specified key file is encrypted the currently installed password
* provider will be used to obtain the decrypt password. * provider will be used to obtain the decrypt password.
* @param sshPrivateKeyFile * @param sshPrivateKeyFile private ssh key file
* @return RSAPrivateKey or DSAPrivateKey * @return private key cipher parameters ({@link RSAKeyParameters} or {@link DSAKeyParameters})
* @throws FileNotFoundException key file not found * @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed * @throws IOException if key file not found or key parse failed
* @see RSAPrivateKey * @throws InvalidKeyException if key is not an SSH private key (i.e., PEM format)
* @see DSAPrivateKey
*/ */
public static Object getSSHPrivateKey(File sshPrivateKeyFile) throws IOException { public static CipherParameters getSSHPrivateKey(File sshPrivateKeyFile)
throws InvalidKeyException, IOException {
if (!sshPrivateKeyFile.isFile()) { if (!sshPrivateKeyFile.isFile()) {
throw new FileNotFoundException("SSH private key file not found: " + sshPrivateKeyFile); throw new FileNotFoundException("SSH private key file not found: " + sshPrivateKeyFile);
} }
InputStream keyIn = new FileInputStream(sshPrivateKeyFile); try (InputStream keyIn = new FileInputStream(sshPrivateKeyFile)) {
try {
return getSSHPrivateKey(keyIn, sshPrivateKeyFile.getAbsolutePath()); return getSSHPrivateKey(keyIn, sshPrivateKeyFile.getAbsolutePath());
} }
finally {
try {
keyIn.close();
}
catch (IOException e) {
}
}
} }
/** /**
* Return the SSH private key corresponding to the specified key input stream. * Return the SSH private key corresponding to the specified key input stream.
* If the specified key is encrypted the currently installed password * If the specified key is encrypted the currently installed password
* provider will be used to obtain the decrypt password. * provider will be used to obtain the decrypt password.
* @param sshPrivateKeyIn * @param sshPrivateKeyIn private ssh key resource input stream
* @return RSAPrivateKey or DSAPrivateKey * @return private key cipher parameters ({@link RSAKeyParameters} or {@link DSAKeyParameters})
* @throws FileNotFoundException key file not found * @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed * @throws IOException if key file not found or key parse failed
* @see RSAPrivateKey * @throws InvalidKeyException if key is not an SSH private key (i.e., PEM format)
* @see DSAPrivateKey
*/ */
public static Object getSSHPrivateKey(InputStream sshPrivateKeyIn) throws IOException { public static CipherParameters getSSHPrivateKey(InputStream sshPrivateKeyIn)
throws InvalidKeyException, IOException {
return getSSHPrivateKey(sshPrivateKeyIn, "Protected SSH Key"); return getSSHPrivateKey(sshPrivateKeyIn, "Protected SSH Key");
} }
private static Object getSSHPrivateKey(InputStream sshPrivateKeyIn, String srcName) private static CipherParameters getSSHPrivateKey(InputStream sshPrivateKeyIn, String srcName)
throws IOException { throws InvalidKeyException, IOException {
boolean isEncrypted = false;
StringBuffer keyBuf = new StringBuffer(); StringBuffer keyBuf = new StringBuffer();
BufferedReader r = new BufferedReader(new InputStreamReader(sshPrivateKeyIn)); try (BufferedReader r = new BufferedReader(new InputStreamReader(sshPrivateKeyIn))) {
String line; boolean checkKeyFormat = true;
while ((line = r.readLine()) != null) { String line;
if (line.startsWith("Proc-Type:")) { while ((line = r.readLine()) != null) {
isEncrypted = (line.indexOf("ENCRYPTED") > 0); 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 * Attempt to instantiate an SSH public key from the specified file
* which contains a single public key. * which contains a single public key.
* @param sshPublicKeyFile * @param sshPublicKeyFile public ssh key file
* @return RSAPublicKey or DSAPublicKey * @return public key cipher parameters {@link RSAKeyParameters} or {@link DSAKeyParameters}
* @throws FileNotFoundException key file not found * @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed * @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 keyLine = null;
String line; try (BufferedReader r = new BufferedReader(new FileReader(sshPublicKeyFile))) {
while ((line = r.readLine()) != null) { String line;
if (!line.startsWith("ssh-")) { while ((line = r.readLine()) != null) {
continue; if (!line.startsWith("ssh-")) {
continue;
}
keyLine = line;
break;
} }
keyLine = line;
break;
} }
r.close();
if (keyLine != null) { if (keyLine != null) {
String[] pieces = keyLine.split(" "); String[] part = keyLine.split("\\s+");
if (pieces.length >= 2) { if (part.length >= 2 && part[0].startsWith("ssh-")) {
byte[] pubkeyBytes = Base64.decode(pieces[1].toCharArray()); byte[] pubkeyBytes = Base64.decode(part[1]);
if ("ssh-rsa".equals(pieces[0])) { return OpenSSHPublicKeyUtil.parsePublicKey(pubkeyBytes);
return RSASHA1Verify.decodeSSHRSAPublicKey(pubkeyBytes);
}
else if ("ssh-dsa".equals(pieces[0])) {
return DSASHA1Verify.decodeSSHDSAPublicKey(pubkeyBytes);
}
} }
} }
throw new IOException( 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); sshPublicKeyFile);
} }

View file

@ -2,7 +2,6 @@
##MODULE IP: Apache License 2.0 ##MODULE IP: Apache License 2.0
##MODULE IP: Bouncy Castle License ##MODULE IP: Bouncy Castle License
##MODULE IP: BSD ##MODULE IP: BSD
##MODULE IP: Christian Plattner
##MODULE IP: Crystal Clear Icons - LGPL 2.1 ##MODULE IP: Crystal Clear Icons - LGPL 2.1
##MODULE IP: FAMFAMFAM Icons - CC 2.5 ##MODULE IP: FAMFAMFAM Icons - CC 2.5
##MODULE IP: JDOM License ##MODULE IP: JDOM License

View file

@ -127,17 +127,16 @@ public class ApplicationKeyManagerFactory {
* This change will take immediate effect for the current executing application, * This change will take immediate effect for the current executing application,
* however, it may still be superseded by a system property setting when running * however, it may still be superseded by a system property setting when running
* the application in the future. See {@link #getKeyStore()}. * 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 * @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) public static synchronized boolean setKeyStore(String path, boolean savePreference) {
throws IOException {
if (System.getProperty(KEYSTORE_PATH_PROPERTY) != null) { if (System.getProperty(KEYSTORE_PATH_PROPERTY) != null) {
Msg.showError(ApplicationKeyManagerFactory.class, null, "Set KeyStore Failed", Msg.showError(ApplicationKeyManagerFactory.class, null, "Set KeyStore Failed",
"KeyStore was set via system property and can not be changed"); "PKI KeyStore was set via system property and can not be changed");
return; return false;
} }
path = prunePath(path); path = prunePath(path);
@ -149,9 +148,11 @@ public class ApplicationKeyManagerFactory {
Preferences.setProperty(KEYSTORE_PATH_PROPERTY, path); Preferences.setProperty(KEYSTORE_PATH_PROPERTY, path);
Preferences.store(); Preferences.store();
} }
return keyInitialized;
} }
catch (CancelledException e) { catch (CancelledException e) {
// ignore - keystore left unchanged // 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 * 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 * wrappedKeyManager will remain null and false will be returned. If an error occurs it
* will be logged and key managers will remain uninitialized. * 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 * @throws CancelledException user cancelled keystore password entry request
*/ */
private synchronized boolean init() throws CancelledException { 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 * wrappedKeyManager will remain null and false will be returned. If an error occurs it
* will be logged and key managers will remain uninitialized. * will be logged and key managers will remain uninitialized.
* @param newKeystorePath specifies the keystore to be opened or null for no keystore * @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 * @throws CancelledException user cancelled keystore password entry request
*/ */
private synchronized boolean init(String newKeystorePath) throws CancelledException { private synchronized boolean init(String newKeystorePath) throws CancelledException {
@ -576,25 +581,25 @@ public class ApplicationKeyManagerFactory {
isSelfSigned = false; isSelfSigned = false;
if (keyManagers.length == 0) { if (keyManagers.length == 0) {
Msg.showError(this, null, "Keystore Failure", Msg.showError(this, null, "PKI Keystore Failure",
"Failed to create key manager: failed to process keystore (no keys processed)"); "Failed to create PKI key manager: failed to process keystore (no keys processed)");
} }
else if (keyManagers.length == 1) { else if (keyManagers.length == 1) {
Msg.showError(this, null, "Keystore Failure", Msg.showError(this, null, "PKI Keystore Failure",
"Failed to create key manager: failed to process keystore (expected X.509)"); "Failed to create PKI key manager: failed to process keystore (expected X.509)");
} }
else { else {
// Unexpected condition // Unexpected condition
Msg.showError(this, null, "Keystore Failure", Msg.showError(this, null, "PKI Keystore Failure",
"Failed to create key manager: unsupported keystore produced multiple KeyManagers"); "Failed to create PKI key manager: unsupported keystore produced multiple KeyManagers");
} }
} }
catch (CancelledException e) { catch (CancelledException e) {
throw e; throw e;
} }
catch (Exception e) { catch (Exception e) {
Msg.showError(this, null, "Keystore Failure", Msg.showError(this, null, "PKI Keystore Failure",
"Failed to create key manager: " + e.getMessage(), e); "Failed to create PKI key manager: " + e.getMessage(), e);
} }
finally { finally {
if (keystoreData != null) { if (keystoreData != null) {

View file

@ -16,7 +16,6 @@
package ghidra.framework.main; package ghidra.framework.main;
import java.io.File; import java.io.File;
import java.io.IOException;
import docking.ActionContext; import docking.ActionContext;
import docking.action.DockingAction; import docking.action.DockingAction;
@ -26,7 +25,6 @@ import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
import ghidra.net.ApplicationKeyManagerFactory; import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import ghidra.util.Msg;
/** /**
* Helper class to manage the actions on the Edit menu. * Helper class to manage the actions on the Edit menu.
@ -121,14 +119,8 @@ class EditActionManager {
return; return;
} }
try { ApplicationKeyManagerFactory.setKeyStore(null, true);
ApplicationKeyManagerFactory.setKeyStore(null, true); clearCertPathAction.setEnabled(false);
clearCertPathAction.setEnabled(false);
}
catch (IOException e) {
Msg.error(this,
"Error occurred while clearing PKI certificate setting: " + e.getMessage());
}
} }
private void editCertPath() { private void editCertPath() {
@ -167,16 +159,9 @@ class EditActionManager {
if (file == null) { if (file == null) {
return; // cancelled return; // cancelled
} }
try { ApplicationKeyManagerFactory.setKeyStore(file.getAbsolutePath(), true);
ApplicationKeyManagerFactory.setKeyStore(file.getAbsolutePath(), true); clearCertPathAction.setEnabled(true);
clearCertPathAction.setEnabled(true); validInput = true;
validInput = true;
}
catch (IOException e) {
Msg.showError(this, tool.getToolFrame(), "Certificate Failure",
"Failed to initialize key manager.\n" + e.getMessage(), e);
file = null;
}
} }
} }

View file

@ -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 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. public key files may be made without restarting the Ghidra Server.
</P> </P>
<P>Each user may generate a suitable SSH key pair with the <typewriter>ssh-keygen</typewriter> command issued from a
shell prompt. A PEM formatted RSA key-pair should be generated using the following command options:
</P>
<PRE>
ssh-keygen -m pem -t rsa -b 2048
</PRE>
<P>NOTE: Ghidra Server authentication does not currently support the OPENSSH key format which may be the default
<typewriter>ssh-keygen</typewriter> format (<typewriter>-m</typewriter> option) on some systems such as Ubuntu.
In addition, other key types (<typewriter>-t</typewriter> option) such as <i>ecdsa</i> and <i>ed25519</i>
are not currently supported.
</P>
(<a href="#top">Back to Top</a>) (<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div> <div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>

View file

@ -16,21 +16,22 @@
package ghidra.server.remote; package ghidra.server.remote;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.security.*; import java.security.*;
import java.security.interfaces.*; import java.security.interfaces.*;
import java.util.Base64; import java.util.Base64;
import ch.ethz.ssh2.packets.TypesWriter; import org.bouncycastle.util.Strings;
public class SSHKeyUtil { 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 * @return kay pair array suitable for writing to SSH private and public
* key files ([0] corresponds to private key, [1] corresponds to public key) * key files ([0] corresponds to private key PEM file, [1] corresponds to public key file)
* @throws NoSuchAlgorithmException * @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"); KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048); generator.initialize(2048);
@ -84,19 +85,64 @@ public class SSHKeyUtil {
out.writeBytes(data); out.writeBytes(data);
} }
private static String getRSAPublicKey(KeyPair rsaKeyPair) { /**
String keyAlgorithm = "ssh-rsa"; * Write UInt32 to an SSH-encoded buffer.
RSAPublicKey rsaPublicKey = (RSAPublicKey) rsaKeyPair.getPublic(); * (modeled after org.bouncycastle.crypto.util.SSHBuilder.u32(int))
TypesWriter w = new TypesWriter(); * @param value integer value
w.writeString(keyAlgorithm); * @param out data output stream
w.writeMPInt(rsaPublicKey.getPublicExponent()); */
w.writeMPInt(rsaPublicKey.getModulus()); 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)); String publicKeyEncoded = new String(Base64.getEncoder().encodeToString(bytesOut));
return keyAlgorithm + " " + publicKeyEncoded + " test\n"; 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) { private static String getRSAPrivateKey(KeyPair rsaKeyPair) {
RSAPrivateKey privateKey = (RSAPrivateKey) rsaKeyPair.getPrivate(); RSAPrivateKey privateKey = (RSAPrivateKey) rsaKeyPair.getPrivate();
RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) privateKey; RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) privateKey;

View file

@ -827,7 +827,7 @@ public class ServerTestUtil {
System.arraycopy(users, 0, userArray, 1, users.length); System.arraycopy(users, 0, userArray, 1, users.length);
createUsers(dirPath, userArray); createUsers(dirPath, userArray);
String keys[] = SSHKeyUtil.generateSSHKeys(); String keys[] = SSHKeyUtil.generateSSHRSAKeys();
addSSHKeys(dirPath, keys[0], "test.key", keys[1], "test.pub"); addSSHKeys(dirPath, keys[0], "test.key", keys[1], "test.pub");
LocalFileSystem repoFilesystem = createRepository(dirPath, "Test", ADMIN_USER + "=ADMIN", LocalFileSystem repoFilesystem = createRepository(dirPath, "Test", ADMIN_USER + "=ADMIN",

View file

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

View file

@ -3,7 +3,6 @@ Apache_License_2.0.txt||LICENSE||||END|
Apache_License_2.0_with_LLVM_Exceptions.txt||LICENSE||||END| Apache_License_2.0_with_LLVM_Exceptions.txt||LICENSE||||END|
BSD.txt||LICENSE||||END| BSD.txt||LICENSE||||END|
Bouncy_Castle_License.txt||LICENSE||||END| Bouncy_Castle_License.txt||LICENSE||||END|
Christian_Plattner.txt||LICENSE||||END|
Creative_Commons_Attribution_2.5.html||LICENSE||||END| Creative_Commons_Attribution_2.5.html||LICENSE||||END|
Crystal_Clear_Icons_-_LGPL_2.1.txt||LICENSE||||END| Crystal_Clear_Icons_-_LGPL_2.1.txt||LICENSE||||END|
FAMFAMFAM_Icons_-_CC_2.5.txt||LICENSE||||END| FAMFAMFAM_Icons_-_CC_2.5.txt||LICENSE||||END|