From e09de8bcb794a9cbcb5e6d60fdcbdef2f95b3756 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 25 Jun 2009 22:46:30 +0000 Subject: Add authentication agent forwarding git-svn-id: https://connectbot.googlecode.com/svn/trunk/connectbot@331 df292f66-193f-0410-a5fc-6d59da041ff2 --- .../java/com/trilead/ssh2/AuthAgentCallback.java | 45 +++ lib/src/main/java/com/trilead/ssh2/Session.java | 23 +- .../ssh2/channel/AuthAgentForwardThread.java | 408 +++++++++++++++++++++ .../com/trilead/ssh2/channel/ChannelManager.java | 55 +++ .../ssh2/packets/PacketChannelAuthAgentReq.java | 33 ++ 5 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/java/com/trilead/ssh2/AuthAgentCallback.java create mode 100644 lib/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java create mode 100644 lib/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java diff --git a/lib/src/main/java/com/trilead/ssh2/AuthAgentCallback.java b/lib/src/main/java/com/trilead/ssh2/AuthAgentCallback.java new file mode 100644 index 0000000..2bd10e0 --- /dev/null +++ b/lib/src/main/java/com/trilead/ssh2/AuthAgentCallback.java @@ -0,0 +1,45 @@ +package com.trilead.ssh2; + +import java.util.Map; + +/** + * AuthAgentCallback. + * + * @author Kenny Root + * @version $Id$ + */ +public interface AuthAgentCallback { + + /** + * @return array of blobs containing the OpenSSH-format encoded public keys + */ + Map retrieveIdentities(); + + /** + * @param key A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + * @param comment comment associated with this key + * @return success or failure + */ + boolean addIdentity(Object key, String comment); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return success or failure + */ + boolean removeIdentity(byte[] publicKey); + + /** + * @return success or failure + */ + boolean removeAllIdentities(); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + */ + Object getPrivateKey(byte[] publicKey); +} diff --git a/lib/src/main/java/com/trilead/ssh2/Session.java b/lib/src/main/java/com/trilead/ssh2/Session.java index 30efa6f..fa0c36f 100644 --- a/lib/src/main/java/com/trilead/ssh2/Session.java +++ b/lib/src/main/java/com/trilead/ssh2/Session.java @@ -353,7 +353,28 @@ public class Session cm.requestChannelTrileadPing(cn); } - + + /** + * Request authentication agent forwarding. + * @param agent object that implements the callbacks + * + * @throws IOException in case of any problem or when the session is closed + */ + public synchronized boolean requestAuthAgentForwarding(AuthAgentCallback agent) throws IOException + { + synchronized (this) + { + /* + * The following is just a nicer error, we would catch it anyway + * later in the channel code + */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + return cm.requestChannelAgentForwarding(cn, agent); + } + public InputStream getStdout() { return cn.getStdoutStream(); diff --git a/lib/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java b/lib/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java new file mode 100644 index 0000000..f517ee4 --- /dev/null +++ b/lib/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java @@ -0,0 +1,408 @@ +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Map.Entry; + +import android.util.Log; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSAPrivateKey; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.DSASignature; +import com.trilead.ssh2.signature.RSAPrivateKey; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.signature.RSASignature; + +/** + * AuthAgentForwardThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class AuthAgentForwardThread extends Thread implements IChannelWorkerThread +{ + public static final int SSH_AGENT_CONSTRAIN_LIFETIME = 1; + public static final int SSH_AGENT_CONSTRAIN_CONFIRM = 2; + + private static final byte[] SSH_AGENT_FAILURE = {0, 0, 0, 1, 5}; + private static final byte[] SSH_AGENT_SUCCESS = {0, 0, 0, 1, 6}; +// public static final int SSH_AGENT_FAILURE = 5; +// public static final int SSH_AGENT_SUCCESS = 6; + + public static final int SSH2_AGENTC_REQUEST_IDENTITIES = 11; + public static final int SSH2_AGENT_IDENTITIES_ANSWER = 12; + + public static final int SSH2_AGENTC_SIGN_REQUEST = 13; + public static final int SSH2_AGENT_SIGN_RESPONSE = 14; + + public static final int SSH2_AGENTC_ADD_IDENTITY = 17; + public static final int SSH2_AGENTC_REMOVE_IDENTITY = 18; + public static final int SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19; + + public static final int SSH_AGENTC_ADD_SMARTCARD_KEY = 20; + public static final int SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21; + + public static final int SSH_AGENTC_LOCK = 22; + public static final int SSH_AGENTC_UNLOCK = 23; + + public static final int SSH2_AGENTC_ADD_ID_CONSTRAINED = 25; + public static final int SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26; + + public static final int SSH_AGENT_OLD_SIGNATURE = 1; + + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + AuthAgentCallback authAgent; + OutputStream os; + InputStream is; + Channel c; + byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + + public AuthAgentForwardThread(Channel c, AuthAgentCallback authAgent) + { + this.c = c; + this.authAgent = authAgent; + + if (log.isEnabled()) + log.log(20, "AuthAgentForwardThread started"); + } + + @Override + public void run() + { + try + { + c.cm.registerThread(this); + } catch (IOException e) { + stopWorking(); + return; + } + + try + { + c.cm.sendOpenConfirmation(c); + + is = c.getStdoutStream(); + os = c.getStdinStream(); + + int totalSize = 4; + int readSoFar = 0; + + while (true) { + int len; + + try + { + len = is.read(buffer, readSoFar, buffer.length - readSoFar); + } + catch (IOException e) { + stopWorking(); + return; + } + + if (len <= 0) + break; + + readSoFar += len; + + Log.d("AuthAgent", "read " + readSoFar + " bytes"); + + if (readSoFar >= 4) { + TypesReader tr = new TypesReader(buffer, 0, 4); + totalSize = tr.readUINT32() + 4; + Log.d("AuthAgent", "message is " + totalSize + " bytes"); + } + + if (totalSize == readSoFar) { +// debugPacket(buffer, readSoFar); + TypesReader tr = new TypesReader(buffer, 4, readSoFar - 4); + int messageType = tr.readByte(); + + Log.d("AuthAgent", "Got a message type " + messageType); + switch (messageType) { + case SSH2_AGENTC_REQUEST_IDENTITIES: + sendIdentities(); + break; + case SSH2_AGENTC_ADD_IDENTITY: + addIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_IDENTITY: + removeIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: + removeAllIdentities(tr); + break; + case SSH2_AGENTC_SIGN_REQUEST: + processSignRequest(tr); + break; + default: + os.write(SSH_AGENT_FAILURE); + break; + } + + readSoFar = 0; + } + // TODO write actual agent forwarding stuff! +// log.log(0, "Received an agent request; sending failure"); +// os.write(AGENT_FAILURE); + } + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + } + catch (IOException e) + { + log.log(50, "IOException in agent forwarder: " + e.getMessage()); + + try + { + is.close(); + } + catch (IOException e1) + { + } + try + { + os.close(); + } + catch (IOException e2) + { + } + try + { + c.cm.closeChannel(c, "IOException in agent forwarder (" + e.getMessage() + ")", true); + } + catch (IOException e3) + { + } + } + } + + public void stopWorking() { + try + { + /* This will lead to an IOException in the is.read() call */ + is.close(); + } + catch (IOException e) + { + } + } + + private void sendIdentities() throws IOException + { + Map keys = authAgent.retrieveIdentities(); + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_IDENTITIES_ANSWER); + int numKeys = 0; + if (keys != null) + numKeys = keys.size(); + tw.writeUINT32(numKeys); + + if (keys != null) { + for (Entry entry : keys.entrySet()) { + byte[] keyBytes = entry.getValue(); + tw.writeString(keyBytes, 0, keyBytes.length); + tw.writeString(entry.getKey()); + } + } + + Log.d("AuthAgent", "Sending " + numKeys + " to server"); + sendPacket(tw.getBytes()); + } + + /** + * @param tr + */ + private void addIdentity(TypesReader tr) { + try + { + String type = tr.readString(); + + Object key; + String comment; + + if (type.equals("ssh-rsa")) { + BigInteger n = tr.readMPINT(); + BigInteger e = tr.readMPINT(); + BigInteger d = tr.readMPINT(); + tr.readMPINT(); // iqmp + tr.readMPINT(); // p + tr.readMPINT(); // q + comment = tr.readString(); + + key = new RSAPrivateKey(d, e, n); + } else if (type.equals("ssh-dss")) { + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + BigInteger x = tr.readMPINT(); + comment = tr.readString(); + + key = new DSAPrivateKey(p, q, g, y, x); + } else { + os.write(SSH_AGENT_FAILURE); + return; + } + + if (authAgent.addIdentity(key, comment)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeIdentity(TypesReader tr) { + try + { + byte[] publicKey = tr.readByteString(); + if (authAgent.removeIdentity(publicKey)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeAllIdentities(TypesReader tr) { + try + { + if (authAgent.removeAllIdentities()) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + private void processSignRequest(TypesReader tr) + { + try + { + byte[] publicKey = tr.readByteString(); + byte[] challenge = tr.readByteString(); + + int flags = tr.readUINT32(); + + Object trileadKey = authAgent.getPrivateKey(publicKey); + + if (trileadKey == null) { + Log.d("AuthAgent", "Key not known to us; failing signature. Public key:"); +// debugPacket(publicKey); + os.write(SSH_AGENT_FAILURE); + return; + } + + byte[] response; + + if (trileadKey instanceof RSAPrivateKey) { + RSASignature signature = RSASHA1Verify.generateSignature(challenge, + (RSAPrivateKey) trileadKey); + response = RSASHA1Verify.encodeSSHRSASignature(signature); + } else if (trileadKey instanceof DSAPrivateKey) { + if ((flags & SSH_AGENT_OLD_SIGNATURE) != 0) + Log.d("AuthAgent", "Want old signature type"); + DSASignature signature = DSASHA1Verify.generateSignature(challenge, + (DSAPrivateKey) trileadKey, new SecureRandom()); + response = DSASHA1Verify.encodeSSHDSASignature(signature); + } else { + Log.d("AuthAgent", "Unknown key type; failing signature request"); + os.write(SSH_AGENT_FAILURE); + return; + } + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_SIGN_RESPONSE); + tw.writeString(response, 0, response.length); + + sendPacket(tw.getBytes()); + } + catch (IOException e) { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) { + } + } + } + + /** + * @param tw + * @throws IOException + */ + private void sendPacket(byte[] message) throws IOException { + TypesWriter packet = new TypesWriter(); + packet.writeUINT32(message.length); + packet.writeBytes(message); +// debugPacket(packet.getBytes()); + os.write(packet.getBytes()); + } + +// private static final char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; +// +// private void debugPacket(byte[] packet) { +// debugPacket(packet, packet.length); +// } +// +// private void debugPacket(byte[] packet, int len) { +// StringBuilder sb = new StringBuilder(); +// sb.append("Packet dump:"); +// +// for (int i = 0; i < len; i++) { +// if (packet[i] < 32 || packet[i] > 0x7e) { +// sb.append(" 0x"); +// sb.append(hexDigits[(packet[i] >> 4) & 0xF]); +// sb.append(hexDigits[packet[i] & 0xF]); +// } else { +// sb.append(" "); +// sb.append((char)packet[i]); +// } +// } +// +// Log.d("AuthAgent", sb.toString()); +// } +} diff --git a/lib/src/main/java/com/trilead/ssh2/channel/ChannelManager.java b/lib/src/main/java/com/trilead/ssh2/channel/ChannelManager.java index fb4beae..630e0cc 100644 --- a/lib/src/main/java/com/trilead/ssh2/channel/ChannelManager.java +++ b/lib/src/main/java/com/trilead/ssh2/channel/ChannelManager.java @@ -5,8 +5,10 @@ import java.io.IOException; import java.util.HashMap; import java.util.Vector; +import com.trilead.ssh2.AuthAgentCallback; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketChannelAuthAgentReq; import com.trilead.ssh2.packets.PacketChannelOpenConfirmation; import com.trilead.ssh2.packets.PacketChannelOpenFailure; import com.trilead.ssh2.packets.PacketChannelTrileadPing; @@ -50,6 +52,8 @@ public class ChannelManager implements MessageHandler private HashMap remoteForwardings = new HashMap(); + private AuthAgentCallback authAgent; + private Vector listenerThreads = new Vector(); private boolean listenerThreadsAllowed = true; @@ -530,6 +534,38 @@ public class ChannelManager implements MessageHandler } + /** + * @param agent + * @throws IOException + */ + public boolean requestChannelAgentForwarding(Channel c, AuthAgentCallback authAgent) throws IOException { + synchronized (this) + { + if (this.authAgent != null) + throw new IllegalStateException("Auth agent already exists"); + + this.authAgent = authAgent; + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + if (log.isEnabled()) + log.log(50, "Requesting agent forwarding"); + + PacketChannelAuthAgentReq aar = new PacketChannelAuthAgentReq(c.remoteID); + tm.sendMessage(aar.getPayload()); + + if (waitForChannelRequestResult(c) == false) { + authAgent = null; + return false; + } + + return true; + } + public void registerThread(IChannelWorkerThread thr) throws IOException { synchronized (listenerThreads) @@ -1274,6 +1310,25 @@ public class ChannelManager implements MessageHandler return; } + if ("auth-agent@openssh.com".equals(channelType)) { + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + AuthAgentForwardThread aat = new AuthAgentForwardThread(c, authAgent); + + aat.setDaemon(true); + aat.start(); + + return; + } + /* Tell the server that we have no idea what it is talking about */ PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, diff --git a/lib/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java b/lib/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java new file mode 100644 index 0000000..95fa396 --- /dev/null +++ b/lib/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +/** + * PacketGlobalAuthAgent. + * + * @author Kenny Root, kenny@the-b.org + * @version $Id$ + */ +public class PacketChannelAuthAgentReq +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelAuthAgentReq(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("auth-agent-req@openssh.com"); + tw.writeBoolean(true); // want reply + payload = tw.getBytes(); + } + return payload; + } +} -- cgit v1.2.3