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); + } + } +}