Skip to content

Commit

Permalink
Merge pull request #195 from bluekeyes/feature/gss-api
Browse files Browse the repository at this point in the history
Add support for "gssapi-with-mic" authentication (Kerberos)
  • Loading branch information
hierynomus committed Jun 16, 2015
2 parents 4cb9610 + b9d0a03 commit 1c5b462
Show file tree
Hide file tree
Showing 10 changed files with 811 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/main/java/net/schmizz/sshj/SSHClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
* <p/>
* 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<Oid> oids = new ArrayList<Oid>(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.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/net/schmizz/sshj/common/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java
Original file line number Diff line number Diff line change
@@ -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<Oid> mechanismOids;
private final GSSManager manager;

private GSSContext secContext;

public AuthGssApiWithMic(LoginContext loginContext, List<Oid> mechanismOids) {
this(loginContext, mechanismOids, GSSManager.getInstance());
}

public AuthGssApiWithMic(LoginContext loginContext, List<Oid> 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<GSSContext> {

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);
}
}
}
66 changes: 66 additions & 0 deletions src/test/java/net/schmizz/sshj/userauth/GssApiTest.java
Original file line number Diff line number Diff line change
@@ -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.<String, Object> 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());
}

}
9 changes: 9 additions & 0 deletions src/test/java/net/schmizz/sshj/util/BasicFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -99,6 +103,7 @@ public boolean authenticate(String u, String p, ServerSession s) {
return false;
}
});
server.setGSSAuthenticator(gssAuthenticator);
server.start();
serverRunning = true;
}
Expand Down Expand Up @@ -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());
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/net/schmizz/sshj/util/gss/BogusGSSAuthenticator.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 1c5b462

Please sign in to comment.