diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java
index a384d08e..92cc3b70 100644
--- a/src/main/java/net/schmizz/sshj/SSHClient.java
+++ b/src/main/java/net/schmizz/sshj/SSHClient.java
@@ -15,6 +15,14 @@
*/
package net.schmizz.sshj;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.Oid;
+
+import com.sun.security.auth.callback.DialogCallbackHandler;
+
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.common.SecurityUtils;
@@ -49,6 +57,7 @@ import net.schmizz.sshj.userauth.keyprovider.KeyFormat;
import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;
+import net.schmizz.sshj.userauth.method.AuthGssApiWithMic;
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
import net.schmizz.sshj.userauth.method.AuthMethod;
import net.schmizz.sshj.userauth.method.AuthPassword;
@@ -63,11 +72,15 @@ import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.PrintStream;
import java.net.ServerSocket;
import java.security.KeyPair;
import java.security.PublicKey;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
@@ -365,6 +378,30 @@ public class SSHClient
authPublickey(username, keyProviders);
}
+ /**
+ * Authenticate {@code username} using the {@code "gssapi-with-mic"} authentication method, given a login context
+ * for the peer GSS machine and a list of supported OIDs.
+ *
+ * Supported OIDs should be ordered by preference as the SSH server will choose the first OID that it also
+ * supports. At least one OID is required
+ *
+ * @param username user to authenticate
+ * @param context {@code LoginContext} for the peer GSS machine
+ * @param supportedOid first supported OID
+ * @param supportedOids other supported OIDs
+ *
+ * @throws UserAuthException in case of authenication failure
+ * @throws TransportException if there was a transport-layer error
+ */
+ public void authGssApiWithMic(String username, LoginContext context, Oid supportedOid, Oid... supportedOids)
+ throws UserAuthException, TransportException {
+ // insert supportedOid to the front of the list since ordering matters
+ List oids = new ArrayList(Arrays.asList(supportedOids));
+ oids.add(0, supportedOid);
+
+ auth(username, new AuthGssApiWithMic(context, oids));
+ }
+
/**
* Disconnects from the connected SSH server. {@code SSHClient} objects are not reusable therefore it is incorrect
* to attempt connection after this method has been called.
diff --git a/src/main/java/net/schmizz/sshj/common/Message.java b/src/main/java/net/schmizz/sshj/common/Message.java
index 01aba157..d019be0e 100644
--- a/src/main/java/net/schmizz/sshj/common/Message.java
+++ b/src/main/java/net/schmizz/sshj/common/Message.java
@@ -46,6 +46,9 @@ public enum Message {
USERAUTH_60(60),
USERAUTH_INFO_RESPONSE(61),
+ USERAUTH_GSSAPI_EXCHANGE_COMPLETE(63),
+ USERAUTH_GSSAPI_MIC(66),
+
GLOBAL_REQUEST(80),
REQUEST_SUCCESS(81),
REQUEST_FAILURE(82),
diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java
new file mode 100644
index 00000000..9ce1adc3
--- /dev/null
+++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java
@@ -0,0 +1,183 @@
+package net.schmizz.sshj.userauth.method;
+
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.List;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+
+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.Oid;
+
+import net.schmizz.sshj.common.Buffer.BufferException;
+import net.schmizz.sshj.common.Buffer.PlainBuffer;
+import net.schmizz.sshj.common.Message;
+import net.schmizz.sshj.common.SSHPacket;
+import net.schmizz.sshj.transport.TransportException;
+import net.schmizz.sshj.userauth.UserAuthException;
+
+/** Implements authentication by GSS-API. */
+public class AuthGssApiWithMic
+ extends AbstractAuthMethod {
+
+ private final LoginContext loginContext;
+ private final List mechanismOids;
+
+ private GSSContext secContext;
+
+ public AuthGssApiWithMic(LoginContext loginContext, List mechanismOids) {
+ super("gssapi-with-mic");
+ this.loginContext = loginContext;
+ this.mechanismOids = mechanismOids;
+
+ secContext = null;
+ }
+
+ @Override
+ public SSHPacket buildReq()
+ throws UserAuthException {
+ SSHPacket packet = super.buildReq() // the generic stuff
+ .putUInt32(mechanismOids.size()); // number of OIDs we support
+ for (Oid oid : mechanismOids) {
+ try {
+ packet.putString(oid.getDER());
+ } catch (GSSException e) {
+ throw new UserAuthException("Mechanism OID could not be encoded: " + oid.toString(), e);
+ }
+ }
+
+ return packet;
+ }
+
+ /**
+ * PrivilegedExceptionAction to be executed within the given LoginContext for
+ * initializing the GSSContext.
+ *
+ * @author Ben Hamme
+ */
+ private class InitializeContextAction implements PrivilegedExceptionAction {
+
+ private final Oid selectedOid;
+
+ public InitializeContextAction(Oid selectedOid) {
+ this.selectedOid = selectedOid;
+ }
+
+ @Override
+ public GSSContext run() throws GSSException {
+ GSSManager manager = GSSManager.getInstance();
+ GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME);
+ GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
+ GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
+
+ GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
+ context.requestMutualAuth(true);
+ context.requestInteg(true);
+
+ return context;
+ }
+ }
+
+ private void sendToken(byte[] token) throws TransportException {
+ SSHPacket packet = new SSHPacket(Message.USERAUTH_INFO_RESPONSE).putString(token);
+ params.getTransport().write(packet);
+ }
+
+ private void handleContextInitialization(SSHPacket buf)
+ throws UserAuthException, TransportException {
+ byte[] bytes;
+ try {
+ bytes = buf.readBytes();
+ } catch (BufferException e) {
+ throw new UserAuthException("Failed to read byte array from message buffer", e);
+ }
+
+ Oid selectedOid;
+ try {
+ selectedOid = new Oid(bytes);
+ } catch (GSSException e) {
+ throw new UserAuthException("Exception constructing OID from server response", e);
+ }
+
+ log.debug("Server selected OID: {}", selectedOid.toString());
+ log.debug("Initializing GSSAPI context");
+
+ Subject subject = loginContext.getSubject();
+
+ try {
+ secContext = Subject.doAs(subject, new InitializeContextAction(selectedOid));
+ } catch (PrivilegedActionException e) {
+ throw new UserAuthException("Exception during context initialization", e);
+ }
+
+ log.debug("Sending initial token");
+ byte[] inToken = new byte[0];
+ try {
+ byte[] outToken = secContext.initSecContext(inToken, 0, inToken.length);
+ sendToken(outToken);
+ } catch (GSSException e) {
+ throw new UserAuthException("Exception sending initial token", e);
+ }
+ }
+
+ private byte[] handleTokenFromServer(SSHPacket buf) throws UserAuthException {
+ byte[] token;
+
+ try {
+ token = buf.readStringAsBytes();
+ } catch (BufferException e) {
+ throw new UserAuthException("Failed to read string from message buffer", e);
+ }
+
+ try {
+ return secContext.initSecContext(token, 0, token.length);
+ } catch (GSSException e) {
+ throw new UserAuthException("Exception during token exchange", e);
+ }
+ }
+
+ private byte[] generateMIC() throws UserAuthException {
+ byte[] msg = new PlainBuffer().putString(params.getTransport().getSessionID())
+ .putByte(Message.USERAUTH_REQUEST.toByte())
+ .putString(params.getUsername())
+ .putString(params.getNextServiceName())
+ .putString(getName())
+ .getCompactData();
+
+ try {
+ return secContext.getMIC(msg, 0, msg.length, null);
+ } catch (GSSException e) {
+ throw new UserAuthException("Exception getting message integrity code", e);
+ }
+ }
+
+ @Override
+ public void handle(Message cmd, SSHPacket buf)
+ throws UserAuthException, TransportException {
+ if (cmd == Message.USERAUTH_60) {
+ handleContextInitialization(buf);
+ } else if (cmd == Message.USERAUTH_INFO_RESPONSE) {
+ byte[] token = handleTokenFromServer(buf);
+
+ if (!secContext.isEstablished()) {
+ log.debug("Sending token");
+ sendToken(token);
+ } else {
+ if (secContext.getIntegState()) {
+ log.debug("Per-message integrity protection available: finalizing authentication with message integrity code");
+ params.getTransport().write(new SSHPacket(Message.USERAUTH_GSSAPI_MIC).putString(generateMIC()));
+ } else {
+ log.debug("Per-message integrity protection unavailable: finalizing authentication");
+ params.getTransport().write(new SSHPacket(Message.USERAUTH_GSSAPI_EXCHANGE_COMPLETE));
+ }
+ }
+ } else {
+ super.handle(cmd, buf);
+ }
+ }
+}