MIME-Version: 1.0 Subject: Re: [JSch-users] GSSAPI support in Jsch From: vadim To: ymnk@jcraft.com In-Reply-To: <200408230044.JAA21673@jcraft.com> Content-Type: text/plain Date: Wed, 25 Aug 2004 00:40:23 +0200 X-Spam-Flag: No X-Spam-Probability: 0.500000 Hallo everybody, please find below promised patch. I will be very glad to get suggestions to improve it. Patch consists of four parts 1) Changes in class com.jcraft.jsch.Session are minor. Here I have introduced several constants as per corresponding internet draft + handling of "gssapi-with-mic" suggestion from ssh server. 2) New class jcraft.jsch.UserAuthGSSAPI handles authentication via GSSAPI. Implementation is based upon mentioned above internet darft and JAAS/GSSAPI tutorial from Sun. 3) Test SSH client 4) Login Module configuration file. In the configuration file I say that the login module has to use TGT from credentials cache if available and to ask for kerberos user id and password otherwise (single sign-on) The implementation has been tested using jdk 1.4.2 i) client on windows 2000 and solaris 8 against OpenSSH 3.8.1p1 build with Heimdal GSSAPI and Heimdal, MIT (linux and solaris), and Microsoft AD as KDC ii) client and server on debian sarge with Heimdal KDC I will be really glad to answer any question .... Best regards, vadim tarassov. Index: src/com/jcraft/jsch/Session.java =================================================================== RCS file: /var/lib/cvs/jsch/src/com/jcraft/jsch/Session.java,v retrieving revision 1.1 diff -u -r1.1 Session.java --- src/com/jcraft/jsch/Session.java 22 Aug 2004 13:24:57 -0000 1.1 +++ src/com/jcraft/jsch/Session.java 23 Aug 2004 00:01:30 -0000 @@ -68,6 +68,13 @@ static final int SSH_MSG_CHANNEL_REQUEST= 98; static final int SSH_MSG_CHANNEL_SUCCESS= 99; static final int SSH_MSG_CHANNEL_FAILURE= 100; + + static final int SSH_MSG_USERAUTH_GSSAPI_RESPONSE = 60; + static final int SSH_MSG_USERAUTH_GSSAPI_TOKEN = 61; + static final int SSH_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = 63; + static final int SSH_MSG_USERAUTH_GSSAPI_ERROR = 64; + static final int SSH_MSG_USERAUTH_GSSAPI_ERRTOK = 65; + static final int SSH_MSG_USERAUTH_GSSAPI_MIC = 66; private byte[] V_S; // server version private byte[] V_C=("SSH-2.0-"+version).getBytes(); // client version @@ -312,6 +319,12 @@ else if(methods.startsWith("password")){ us=new UserAuthPassword(userinfo); } + + // GSSAPI support + else if(methods.startsWith("gssapi-with-mic")){ + + us = new UserAuthGSSAPI(userinfo); + } if(us!=null){ try{ auth=us.start(this); Index: login.conf =================================================================== RCS file: login.conf diff -N login.conf --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ login.conf 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,4 @@ +com.sun.security.jgss.initiate { + com.sun.security.auth.module.Krb5LoginModule required + useTicketCache=true; +}; Index: examples/com/winterthur/ratte/jssh/Shell.java =================================================================== RCS file: examples/com/winterthur/ratte/jssh/Shell.java diff -N examples/com/winterthur/ratte/jssh/Shell.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ examples/com/winterthur/ratte/jssh/Shell.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,115 @@ +/* + * Created on 12.07.2004 + * + * To change the template for this generated file go to + * Window>Preferences>Java>Code Generation>Code and Comments + */ +package com.winterthur.ratte.jssh; + +/* -*-mode:java; c-basic-offset:2; -*- */ +import java.security.Security; + +import javax.swing.JOptionPane; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +public class Shell{ + + public static void main(String[] arg){ + + try{ + + JSch jsch=new JSch(); + + //jsch.setKnownHosts("/home/foo/.ssh/known_hosts"); + + String host=JOptionPane.showInputDialog("Enter username@hostname", + System.getProperty("user.name")+ + "@localhost.localdomain"); + + String user = host.substring(0, host.indexOf('@')); + host = host.substring(host.indexOf('@')+1); + + Session session = jsch.getSession(user, host, 22); + + //session.setPassword("your password"); + + // username and password will be given via UserInfo interface. + + UserInfo ui = new MyUserInfo(); + session.setUserInfo(ui); + + //java.util.Hashtable config=new java.util.Hashtable(); + //config.put("StrictHostKeyChecking", "no"); + //session.setConfig(config); + + session.connect(); + + Channel channel=session.openChannel("shell"); + + channel.setInputStream(System.in); + channel.setOutputStream(System.out); + + channel.connect(); + }catch(Exception e){ + + System.out.println(e); + } + } + + public static class MyUserInfo implements UserInfo{ + + public String getPassword(){ + return passwd; + } + + public boolean promptYesNo(String str){ + + Object[] options={ "yes", "no" }; + int foo = JOptionPane.showOptionDialog(null, str, "Warning", + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, options, options[0]); + return foo==0; + } + + String passwd; + JTextField passwordField=(JTextField)new JPasswordField(20); + + public String getPassphrase(){ + + return null; + } + + public boolean promptPassphrase(String message){ + + return true; + } + + public boolean promptPassword(String message){ + + Object[] ob = {passwordField}; + int result = JOptionPane.showConfirmDialog(null, ob, message, + JOptionPane.OK_CANCEL_OPTION); + if(result == JOptionPane.OK_OPTION){ + passwd = passwordField.getText(); + return true; + }else{ + + return false; + } + } + + public void showMessage(String message){ + + JOptionPane.showMessageDialog(null, message); + } + } +} + + Index: src/com/jcraft/jsch/UserAuthGSSAPI.java =================================================================== RCS file: src/com/jcraft/jsch/UserAuthGSSAPI.java diff -N src/com/jcraft/jsch/UserAuthGSSAPI.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/com/jcraft/jsch/UserAuthGSSAPI.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,235 @@ +/* + * Created on Aug 22, 2004 + * + * TODO To change the template for this generated file go to + * Window - Preferences - Java - Code Style - Code Templates + */ +package com.jcraft.jsch; + +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.MessageProp; +import org.ietf.jgss.Oid; + +/** + * @author vadim + * + * TODO To change the template for this generated type comment go to + * Window - Preferences - Java - Code Style - Code Templates + */ +public class UserAuthGSSAPI extends UserAuth { + + public UserAuthGSSAPI(UserInfo ui){ + } + + public boolean start(Session session){ + + Oid krb5Oid; + Packet packet = session.packet; + Buffer buffer = session.buf; + + packet.reset(); + + /* + * 3.2 Initiating GSSAPI authentication + * + * The GSSAPI authentication method is initiated when the client + * sends a SSH_MSG_USERAUTH_REQUEST + * + * byte SSH_MSG_USERAUTH_REQUEST + * string user name (in ISO-10646 UTF-8 encoding) + * string service name (in US-ASCII) + * string "gssapi" (US-ASCII) + * uint32 n, the number of OIDs client supports + * string[n] mechanism OIDS + */ + + buffer.putByte((byte)Session.SSH_MSG_USERAUTH_REQUEST); + + byte[] _username = null; + + try{ + _username = session.username.getBytes("UTF-8"); + }catch(java.io.UnsupportedEncodingException e){ + _username = session.username.getBytes(); + } + + buffer.putString(_username); + buffer.putString("ssh-connection".getBytes()); + buffer.putString("gssapi-with-mic".getBytes()); + buffer.putInt(1); + + try{ + krb5Oid = new Oid("1.2.840.113554.1.2.2"); + buffer.putString(krb5Oid.getDER()); + }catch(GSSException ex){ + ex.printStackTrace(); + return false; + } + + try{ + session.write(packet); + }catch(Exception ex){ + ex.printStackTrace(); + return false; + } + + try{ + + buffer = session.read(buffer); + + if(buffer.buffer[5] == Session.SSH_MSG_USERAUTH_GSSAPI_RESPONSE){ + buffer.getInt(); + buffer.getByte(); + buffer.getByte(); + byte[] message = buffer.getString(); + + if(!krb5Oid.equals(new Oid(message))){ + return false; + } + }else{ + return false; + } + + }catch(Exception ex){ + ex.printStackTrace(); + return false; + } + + /* + * This Oid is used to represent the Kerberos version 5 GSS-API + * mechanism. It is defined in RFC 1964. We will use this Oid + * whenever we need to indicate to the GSS-API that it must + * use Kerberos for some purpose. + */ + + + try{ + + GSSManager manager = GSSManager.getInstance(); + + /* + * Create a GSSName out of the server's name. The null + * indicates that this application does not wish to make + * any claims about the syntax of this name and that the + * underlying mechanism should try to parse it as per whatever + * default syntax it chooses. + */ + + Oid krb5PrincipalNameType = new Oid("1.2.840.113554.1.2.2.1"); + + GSSName serverName = manager.createName("host/" + session.host, + krb5PrincipalNameType); + + GSSName myName = manager.createName(session.username, krb5PrincipalNameType); + + GSSCredential myCred = manager.createCredential(myName, + GSSCredential.DEFAULT_LIFETIME, + krb5Oid, + GSSCredential.INITIATE_ONLY); + + + /* + * Create a GSSContext for mutual authentication with the + * server. + * - serverName is the GSSName that represents the server. + * - krb5Oid is the Oid that represents the mechanism to + * use. The client chooses the mechanism to use. + * - null is passed in for client credentials + * - DEFAULT_LIFETIME lets the mechanism decide how long the + * context can remain valid. + * Note: Passing in null for the credentials asks GSS-API to + * use the default credentials. This means that the mechanism + * will look among the credentials stored in the current Subject + * to find the right kind of credentials that it needs. + */ + + GSSContext context = manager.createContext(serverName, + krb5Oid, + myCred, + GSSContext.DEFAULT_LIFETIME); + + // Set the desired optional features on the context. The client + // chooses these options. + + context.requestMutualAuth(true); // Mutual authentication + context.requestConf(true); // Will use confidentiality later + context.requestInteg(true); // Will use integrity later + context.requestCredDeleg(true); + context.requestAnonymity(false); + + // Do the context eastablishment loop + + byte[] token = new byte[0]; + + while (!context.isEstablished()) { + + // token is ignored on the first call + token = context.initSecContext(token, 0, token.length); + + // Send a token to the server if one was generated by + // initSecContext + + if (token != null) { + + packet.reset(); + buffer.putByte((byte)Session.SSH_MSG_USERAUTH_GSSAPI_TOKEN); + buffer.putString(token); + session.write(packet); + } + + // If the client is done with context establishment + // then there will be no more tokens to read in this loop + + if (!context.isEstablished()) { + + buffer = session.read(buffer); + + if(buffer.buffer[5] == Session.SSH_MSG_USERAUTH_GSSAPI_ERROR){ + return false; + } + + buffer.getInt(); + buffer.getByte(); + buffer.getByte(); + + token = buffer.getString(); + } + } + + /* + * Build MIC + */ + + Buffer micBuffer = new Buffer(); + micBuffer.putString(session.getSessionId()); + micBuffer.putByte((byte)Session.SSH_MSG_USERAUTH_REQUEST); + micBuffer.putString(_username); + micBuffer.putString("ssh-connection".getBytes()); + micBuffer.putString("gssapi-with-mic".getBytes()); + + MessageProp prop = new MessageProp(0, true); + + byte[] mic = context.getMIC(micBuffer.buffer, 0, micBuffer.getLength(), prop); + + packet.reset(); + buffer.putByte((byte)Session.SSH_MSG_USERAUTH_GSSAPI_MIC); + buffer.putString(mic); + session.write(packet); + + buffer = session.read(buffer); + + }catch(GSSException ex){ + ex.printStackTrace(); + return false; + }catch(Exception ex){ + ex.printStackTrace(); + return false; + } + + return true; + } +}