diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java
index a384d08e5..a59eaa431 100644
--- a/src/main/java/net/schmizz/sshj/SSHClient.java
+++ b/src/main/java/net/schmizz/sshj/SSHClient.java
@@ -49,6 +49,7 @@
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;
@@ -58,15 +59,19 @@
import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.userauth.password.Resource;
import net.schmizz.sshj.xfer.scp.SCPFileTransfer;
+import org.ietf.jgss.Oid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.security.auth.login.LoginContext;
+
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.security.KeyPair;
import java.security.PublicKey;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
@@ -365,6 +370,30 @@ public void authPublickey(String username, String... locations)
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 authentication 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 01aba1578..d019be0ef 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 000000000..eb800a13b
--- /dev/null
+++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java
@@ -0,0 +1,188 @@
+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 final GSSManager manager;
+
+ private GSSContext secContext;
+
+ public AuthGssApiWithMic(LoginContext loginContext, List mechanismOids) {
+ this(loginContext, mechanismOids, GSSManager.getInstance());
+ }
+
+ public AuthGssApiWithMic(LoginContext loginContext, List mechanismOids, GSSManager manager) {
+ super("gssapi-with-mic");
+ this.loginContext = loginContext;
+ this.mechanismOids = mechanismOids;
+ this.manager = manager;
+
+ 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 {
+ 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);
+ }
+ }
+}
diff --git a/src/test/java/net/schmizz/sshj/userauth/GssApiTest.java b/src/test/java/net/schmizz/sshj/userauth/GssApiTest.java
new file mode 100644
index 000000000..27e744a38
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/userauth/GssApiTest.java
@@ -0,0 +1,66 @@
+package net.schmizz.sshj.userauth;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import net.schmizz.sshj.userauth.method.AuthGssApiWithMic;
+import net.schmizz.sshj.util.BasicFixture;
+import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
+import net.schmizz.sshj.util.gss.BogusGSSManager;
+
+public class GssApiTest {
+
+ private static final String LOGIN_CONTEXT_NAME = "TestLoginContext";
+
+ private static class TestAuthConfiguration extends Configuration {
+ private AppConfigurationEntry entry = new AppConfigurationEntry(
+ "testLoginModule",
+ LoginModuleControlFlag.REQUIRED,
+ Collections. emptyMap());
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ if (name.equals(LOGIN_CONTEXT_NAME)) {
+ return new AppConfigurationEntry[] { entry };
+ } else {
+ return new AppConfigurationEntry[0];
+ }
+ }
+ }
+
+ private final BasicFixture fixture = new BasicFixture();
+
+ @Before
+ public void setUp() throws Exception {
+ fixture.setGssAuthenticator(new BogusGSSAuthenticator());
+ fixture.init(false);
+ }
+
+ @After
+ public void tearDown() throws IOException, InterruptedException {
+ fixture.done();
+ }
+
+ @Test
+ public void authenticated() throws Exception {
+ AuthGssApiWithMic authMethod = new AuthGssApiWithMic(
+ new LoginContext(LOGIN_CONTEXT_NAME, null, null, new TestAuthConfiguration()),
+ Collections.singletonList(BogusGSSManager.KRB5_MECH),
+ new BogusGSSManager());
+
+ fixture.getClient().auth("user", authMethod);
+ assertTrue(fixture.getClient().isAuthenticated());
+ }
+
+}
diff --git a/src/test/java/net/schmizz/sshj/util/BasicFixture.java b/src/test/java/net/schmizz/sshj/util/BasicFixture.java
index 11f781841..910697d93 100644
--- a/src/test/java/net/schmizz/sshj/util/BasicFixture.java
+++ b/src/test/java/net/schmizz/sshj/util/BasicFixture.java
@@ -33,9 +33,11 @@
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.userauth.UserAuthException;
+
import org.apache.sshd.SshServer;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.server.PasswordAuthenticator;
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import java.io.IOException;
@@ -50,6 +52,8 @@ public class BasicFixture {
public static final String hostname = "localhost";
public final int port = gimmeAPort();
+ private GSSAuthenticator gssAuthenticator;
+
private SSHClient client;
private SshServer server;
@@ -99,6 +103,7 @@ public boolean authenticate(String u, String p, ServerSession s) {
return false;
}
});
+ server.setGSSAuthenticator(gssAuthenticator);
server.start();
serverRunning = true;
}
@@ -137,6 +142,10 @@ public SSHClient getClient() {
return client;
}
+ public void setGssAuthenticator(GSSAuthenticator gssAuthenticator) {
+ this.gssAuthenticator = gssAuthenticator;
+ }
+
public void dummyAuth()
throws UserAuthException, TransportException {
server.setPasswordAuthenticator(new BogusPasswordAuthenticator());
diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSAuthenticator.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSAuthenticator.java
new file mode 100644
index 000000000..9528b390a
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSAuthenticator.java
@@ -0,0 +1,22 @@
+package net.schmizz.sshj.util.gss;
+
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+
+public class BogusGSSAuthenticator
+ extends GSSAuthenticator {
+
+ private final GSSManager manager = new BogusGSSManager();
+
+ @Override
+ public GSSManager getGSSManager() {
+ return manager;
+ }
+
+ @Override
+ public GSSCredential getGSSCredential(GSSManager mgr) throws GSSException {
+ return manager.createCredential(GSSCredential.ACCEPT_ONLY);
+ }
+}
diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java
new file mode 100644
index 000000000..597768d59
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSContext.java
@@ -0,0 +1,243 @@
+package net.schmizz.sshj.util.gss;
+
+import static net.schmizz.sshj.util.gss.BogusGSSManager.unavailable;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+import org.ietf.jgss.ChannelBinding;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.MessageProp;
+import org.ietf.jgss.Oid;
+
+public class BogusGSSContext
+ implements GSSContext {
+
+ private static final byte[] INIT_TOKEN = fromString("INIT");
+ private static final byte[] ACCEPT_TOKEN = fromString("ACCEPT");
+ private static final byte[] MIC = fromString("LGTM");
+
+ private static byte[] fromString(String s) {
+ return s.getBytes(Charset.forName("UTF-8"));
+ }
+
+ private boolean initialized = false;
+ private boolean accepted = false;
+ private boolean integState = false;
+ private boolean mutualAuthState = false;
+
+ @Override
+ public byte[] initSecContext(byte[] inputBuf, int offset, int len) throws GSSException {
+ initialized = true;
+ return INIT_TOKEN;
+ }
+
+ @Override
+ public int initSecContext(InputStream inStream, OutputStream outStream) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public byte[] acceptSecContext(byte[] inToken, int offset, int len) throws GSSException {
+ accepted = Arrays.equals(INIT_TOKEN, Arrays.copyOfRange(inToken, offset, offset + len));
+ return ACCEPT_TOKEN;
+ }
+
+ @Override
+ public void acceptSecContext(InputStream inStream, OutputStream outStream) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public boolean isEstablished() {
+ return initialized || accepted;
+ }
+
+ @Override
+ public void dispose() throws GSSException {}
+
+ @Override
+ public int getWrapSizeLimit(int qop, boolean confReq, int maxTokenSize) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public byte[] wrap(byte[] inBuf, int offset, int len, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void wrap(InputStream inStream, OutputStream outStream, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public byte[] unwrap(byte[] inBuf, int offset, int len, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void unwrap(InputStream inStream, OutputStream outStream, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public byte[] getMIC(byte[] inMsg, int offset, int len, MessageProp msgProp) throws GSSException {
+ return MIC;
+ }
+
+ @Override
+ public void getMIC(InputStream inStream, OutputStream outStream, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void verifyMIC(byte[] inToken, int tokOffset, int tokLen, byte[] inMsg, int msgOffset, int msgLen, MessageProp msgProp) throws GSSException {
+ if (!Arrays.equals(MIC, Arrays.copyOfRange(inToken, tokOffset, tokOffset + tokLen))) {
+ throw new GSSException(GSSException.BAD_MIC);
+ }
+ }
+
+ @Override
+ public void verifyMIC(InputStream tokStream, InputStream msgStream, MessageProp msgProp) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public byte[] export() throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestMutualAuth(boolean state) throws GSSException {
+ this.mutualAuthState = state;
+ }
+
+ @Override
+ public void requestInteg(boolean state) throws GSSException {
+ this.integState = state;
+ }
+
+ @Override
+ public void requestReplayDet(boolean state) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestSequenceDet(boolean state) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestCredDeleg(boolean state) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestAnonymity(boolean state) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestConf(boolean state) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void requestLifetime(int lifetime) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void setChannelBinding(ChannelBinding cb) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public boolean getMutualAuthState() {
+ return mutualAuthState;
+ }
+
+ @Override
+ public boolean getIntegState() {
+ return integState;
+ }
+
+ @Override
+ public boolean getCredDelegState() {
+ return false;
+ }
+
+ @Override
+ public boolean getReplayDetState() {
+ return false;
+ }
+
+ @Override
+ public boolean getSequenceDetState() {
+ return false;
+ }
+
+ @Override
+ public boolean getAnonymityState() {
+ return false;
+ }
+
+ @Override
+ public boolean isTransferable() throws GSSException {
+ return false;
+ }
+
+ @Override
+ public boolean isProtReady() {
+ return false;
+ }
+
+ @Override
+ public boolean getConfState() {
+ return false;
+ }
+
+ @Override
+ public int getLifetime() {
+ return INDEFINITE_LIFETIME;
+ }
+
+ @Override
+ public GSSName getSrcName() throws GSSException {
+ try {
+ String hostname = InetAddress.getLocalHost().getCanonicalHostName();
+ return new BogusGSSName("user@" + hostname, GSSName.NT_HOSTBASED_SERVICE);
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public GSSName getTargName() throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public Oid getMech() throws GSSException {
+ return BogusGSSManager.KRB5_MECH;
+ }
+
+ @Override
+ public GSSCredential getDelegCred() throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public boolean isInitiator() throws GSSException {
+ return false;
+ }
+
+}
diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSCredential.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSCredential.java
new file mode 100644
index 000000000..c1d9803b7
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSCredential.java
@@ -0,0 +1,87 @@
+package net.schmizz.sshj.util.gss;
+
+import static net.schmizz.sshj.util.gss.BogusGSSManager.unavailable;
+
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+public class BogusGSSCredential
+ implements GSSCredential {
+
+ private final GSSName name;
+ private final int usage;
+
+ public BogusGSSCredential(GSSName name, int usage) {
+ this.name = name;
+ this.usage = usage;
+ }
+
+ @Override
+ public void dispose() throws GSSException {}
+
+ @Override
+ public GSSName getName() throws GSSException {
+ return name;
+ }
+
+ @Override
+ public GSSName getName(Oid mech) throws GSSException {
+ return name.canonicalize(mech);
+ }
+
+ @Override
+ public int getRemainingLifetime() throws GSSException {
+ return INDEFINITE_LIFETIME;
+ }
+
+ @Override
+ public int getRemainingInitLifetime(Oid mech) throws GSSException {
+ return INDEFINITE_LIFETIME;
+ }
+
+ @Override
+ public int getRemainingAcceptLifetime(Oid mech) throws GSSException {
+ return INDEFINITE_LIFETIME;
+ }
+
+ @Override
+ public int getUsage() throws GSSException {
+ return usage;
+ }
+
+ @Override
+ public int getUsage(Oid mech) throws GSSException {
+ return usage;
+ }
+
+ @Override
+ public Oid[] getMechs() throws GSSException {
+ return new Oid[] { BogusGSSManager.KRB5_MECH };
+ }
+
+ @Override
+ public void add(GSSName name, int initLifetime, int acceptLifetime, Oid mech, int usage) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return (name == null ? 0 : name.hashCode());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof BogusGSSCredential)) {
+ return false;
+ }
+ GSSName otherName = ((BogusGSSCredential) obj).name;
+ return name == null ? otherName == null : name.equals((Object) otherName);
+ }
+}
diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSManager.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSManager.java
new file mode 100644
index 000000000..c083ecfe3
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSManager.java
@@ -0,0 +1,106 @@
+package net.schmizz.sshj.util.gss;
+
+import java.security.Provider;
+
+import org.apache.sshd.server.auth.gss.UserAuthGSS;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements a fake Kerberos 5 mechanism. MINA only supports Kerberos 5 over
+ * GSS-API, so we can't implement a separate mechanism.
+ */
+public class BogusGSSManager
+ extends GSSManager {
+
+ public static final Oid KRB5_MECH = UserAuthGSS.KRB5_MECH;
+
+ private static final Logger log = LoggerFactory.getLogger(BogusGSSManager.class);
+
+ @Override
+ public Oid[] getMechs() {
+ return new Oid[] { KRB5_MECH };
+ }
+
+ @Override
+ public Oid[] getNamesForMech(Oid mech) throws GSSException {
+ return new Oid[] { GSSName.NT_EXPORT_NAME, GSSName.NT_HOSTBASED_SERVICE };
+ }
+
+ @Override
+ public Oid[] getMechsForName(Oid nameType) {
+ return new Oid[] { KRB5_MECH };
+ }
+
+ @Override
+ public GSSName createName(String nameStr, Oid nameType) throws GSSException {
+ return new BogusGSSName(nameStr, nameType);
+ }
+
+ @Override
+ public GSSName createName(byte[] name, Oid nameType) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public GSSName createName(String nameStr, Oid nameType, Oid mech) throws GSSException {
+ return this.createName(nameStr, nameType);
+ }
+
+ @Override
+ public GSSName createName(byte[] name, Oid nameType, Oid mech) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public GSSCredential createCredential(int usage) throws GSSException {
+ return new BogusGSSCredential(null, usage);
+ }
+
+ @Override
+ public GSSCredential createCredential(GSSName name, int lifetime, Oid mech, int usage) throws GSSException {
+ return new BogusGSSCredential(name, usage);
+ }
+
+ @Override
+ public GSSCredential createCredential(GSSName name, int lifetime, Oid[] mechs, int usage) throws GSSException {
+ return new BogusGSSCredential(name, usage);
+ }
+
+ @Override
+ public GSSContext createContext(GSSName peer, Oid mech, GSSCredential myCred, int lifetime) throws GSSException {
+ return new BogusGSSContext();
+ }
+
+ @Override
+ public GSSContext createContext(GSSCredential myCred) throws GSSException {
+ return new BogusGSSContext();
+ }
+
+ @Override
+ public GSSContext createContext(byte[] interProcessToken) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void addProviderAtFront(Provider p, Oid mech) throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public void addProviderAtEnd(Provider p, Oid mech) throws GSSException {
+ throw unavailable();
+ }
+
+ static GSSException unavailable() throws GSSException {
+ GSSException e = new GSSException(GSSException.UNAVAILABLE);
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/net/schmizz/sshj/util/gss/BogusGSSName.java b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSName.java
new file mode 100644
index 000000000..8b39864a7
--- /dev/null
+++ b/src/test/java/net/schmizz/sshj/util/gss/BogusGSSName.java
@@ -0,0 +1,58 @@
+package net.schmizz.sshj.util.gss;
+
+import static net.schmizz.sshj.util.gss.BogusGSSManager.unavailable;
+
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+public class BogusGSSName
+ implements GSSName {
+
+ private final String name;
+ private final Oid oid;
+
+ public BogusGSSName(String name, Oid oid) {
+ this.name = name;
+ this.oid = oid;
+ }
+
+ @Override
+ public boolean equals(GSSName another) throws GSSException {
+ if (!(another instanceof BogusGSSName)) {
+ throw new GSSException(GSSException.BAD_NAMETYPE);
+ }
+ BogusGSSName otherName = (BogusGSSName) another;
+ return name.equals(otherName.name) && oid.equals(otherName.oid);
+ }
+
+ @Override
+ public GSSName canonicalize(Oid mech) throws GSSException {
+ return this;
+ }
+
+ @Override
+ public byte[] export() throws GSSException {
+ throw unavailable();
+ }
+
+ @Override
+ public Oid getStringNameType() throws GSSException {
+ return oid;
+ }
+
+ @Override
+ public boolean isAnonymous() {
+ return false;
+ }
+
+ @Override
+ public boolean isMN() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}