-
Notifications
You must be signed in to change notification settings - Fork 606
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #195 from bluekeyes/feature/gss-api
Add support for "gssapi-with-mic" authentication (Kerberos)
- Loading branch information
Showing
10 changed files
with
811 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
188 changes: 188 additions & 0 deletions
188
src/main/java/net/schmizz/sshj/userauth/method/AuthGssApiWithMic.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
src/test/java/net/schmizz/sshj/util/gss/BogusGSSAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.