diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF index e44ee0301..044576fcc 100644 --- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF @@ -8,17 +8,22 @@ Bundle-Localization: plugin Bundle-Vendor: %provider_name Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.apache.sshd.common;version="[2.0.0,2.1.0)", +Import-Package: org.apache.sshd.common;version="[2.0.0,2.1.0)", org.apache.sshd.common.config.keys;version="[2.0.0,2.1.0)", org.apache.sshd.common.file.virtualfs;version="[2.0.0,2.1.0)", org.apache.sshd.common.helpers;version="[2.0.0,2.1.0)", + org.apache.sshd.common.io;version="[2.0.0,2.1.0)", org.apache.sshd.common.kex;version="[2.0.0,2.1.0)", org.apache.sshd.common.keyprovider;version="[2.0.0,2.1.0)", org.apache.sshd.common.session;version="[2.0.0,2.1.0)", + org.apache.sshd.common.util.buffer;version="[2.0.0,2.1.0)", org.apache.sshd.common.util.logging;version="[2.0.0,2.1.0)", org.apache.sshd.common.util.security;version="[2.0.0,2.1.0)", org.apache.sshd.server;version="[2.0.0,2.1.0)", + org.apache.sshd.server.auth;version="[2.0.0,2.1.0)", + org.apache.sshd.server.auth.gss;version="[2.0.0,2.1.0)", org.apache.sshd.server.command;version="[2.0.0,2.1.0)", + org.apache.sshd.server.session;version="[2.0.0,2.1.0)", org.apache.sshd.server.shell;version="[2.0.0,2.1.0)", org.apache.sshd.server.subsystem.sftp;version="[2.0.0,2.1.0)", org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)", diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java index 8d3207c43..3c1111d24 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java @@ -49,19 +49,30 @@ import java.security.KeyPair; import java.security.PublicKey; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.server.ServerAuthenticationManager; import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.UserAuth; +import org.apache.sshd.server.auth.gss.GSSAuthenticator; +import org.apache.sshd.server.auth.gss.UserAuthGSS; +import org.apache.sshd.server.auth.gss.UserAuthGSSFactory; import org.apache.sshd.server.command.AbstractCommandSupport; +import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.shell.UnknownCommand; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; import org.eclipse.jgit.annotations.NonNull; @@ -142,6 +153,7 @@ protected Path computeRootDir(Session session) throws IOException { .getParentFile().getAbsoluteFile().toPath(); } }); + server.setUserAuthFactories(getAuthFactories()); server.setSubsystemFactories(Collections .singletonList((new SftpSubsystemFactory.Builder()).build())); // No shell @@ -149,8 +161,15 @@ protected Path computeRootDir(Session session) throws IOException { // Disable some authentications server.setPasswordAuthenticator(null); server.setKeyboardInteractiveAuthenticator(null); - server.setGSSAuthenticator(null); server.setHostBasedAuthenticator(null); + // Pretend we did gssapi-with-mic. + server.setGSSAuthenticator(new GSSAuthenticator() { + @Override + public boolean validateInitialUser(ServerSession session, + String user) { + return false; + } + }); // Accept only the test user/public key server.setPublickeyAuthenticator((userName, publicKey, session) -> { return SshTestGitServer.this.testUser.equals(userName) && KeyUtils @@ -166,6 +185,40 @@ protected Path computeRootDir(Session session) throws IOException { }); } + private static class FakeUserAuthGSS extends UserAuthGSS { + @Override + protected Boolean doAuth(Buffer buffer, boolean initial) + throws Exception { + // We always reply that we did do this, but then we fail at the + // first token message. That way we can test that the client-side + // sends the correct initial request and then is skipped correctly, + // even if it causes a GSSException if Kerberos isn't configured at + // all. + if (initial) { + ServerSession session = getServerSession(); + Buffer b = session.createBuffer( + SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST); + b.putBytes(KRB5_MECH.getDER()); + session.writePacket(b); + return null; + } + return Boolean.FALSE; + } + } + + private List> getAuthFactories() { + List> authentications = new ArrayList<>(); + authentications.add( + ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY); + authentications.add(new UserAuthGSSFactory() { + @Override + public UserAuth create() { + return new FakeUserAuthGSS(); + } + }); + return authentications; + } + /** * Starts the test server, listening on a random port. * diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index 72bca6a97..963e3d95f 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -2,6 +2,10 @@ authenticationCanceled=Authentication canceled: no password closeListenerFailed=Ssh session close listener failed configInvalidPath=Invalid path in ssh config key {0}: {1} ftpCloseFailed=Closing the SFTP channel failed +gssapiFailure=GSS-API error for mechanism OID {0} +gssapiInitFailure=GSS-API initialization failure for mechanism {0} +gssapiUnexpectedMechanism=Server {0} replied with unknown mechanism name ''{1}'' in {2} authentication +gssapiUnexpectedMessage=Received unexpected ssh message {1} in {0} authentication keyEncryptedMsg=Key ''{0}'' is encrypted. Enter the passphrase to decrypt it. keyEncryptedPrompt=Passphrase keyLoadFailed=Could not load key ''{0}'' diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java new file mode 100644 index 000000000..834a50309 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jgit.annotations.NonNull; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + * Global repository of GSS-API mechanisms that we can use. + */ +public class GssApiMechanisms { + + private GssApiMechanisms() { + // No instantiation + } + + /** Prefix to use with {@link GSSName#NT_HOSTBASED_SERVICE}. */ + public static final String GSSAPI_HOST_PREFIX = "host@"; //$NON-NLS-1$ + + /** The {@link Oid} of Kerberos 5. */ + public static final Oid KERBEROS_5 = createOid("1.2.840.113554.1.2.2"); //$NON-NLS-1$ + + /** SGNEGO is not to be used with ssh. */ + private static final Oid SPNEGO = createOid("1.3.6.1.5.5.2"); //$NON-NLS-1$ + + /** Protects {@link #supportedMechanisms}. */ + private static final Object LOCK = new Object(); + + /** + * The {@link AtomicBoolean} is set to {@code true} when the mechanism could + * be initialized successfully at least once. + */ + private static Map supportedMechanisms; + + /** + * Retrieves an immutable collection of the supported mechanisms. + * + * @return the supported mechanisms + */ + @NonNull + public static Collection getSupportedMechanisms() { + synchronized (LOCK) { + if (supportedMechanisms == null) { + GSSManager manager = GSSManager.getInstance(); + Oid[] mechs = manager.getMechs(); + Map mechanisms = new LinkedHashMap<>(); + if (mechs != null) { + for (Oid oid : mechs) { + // RFC 4462 states that SPNEGO must not be used with ssh + if (!SPNEGO.equals(oid)) { + mechanisms.put(oid, Boolean.FALSE); + } + } + } + supportedMechanisms = mechanisms; + } + return Collections.unmodifiableSet(supportedMechanisms.keySet()); + } + } + + /** + * Report that this mechanism was used successfully. + * + * @param mechanism + * that worked + */ + public static void worked(@NonNull Oid mechanism) { + synchronized (LOCK) { + supportedMechanisms.put(mechanism, Boolean.TRUE); + } + } + + /** + * Mark the mechanisms as failed. + * + * @param mechanism + * to mark + */ + public static void failed(@NonNull Oid mechanism) { + synchronized (LOCK) { + Boolean worked = supportedMechanisms.get(mechanism); + if (worked != null && !worked.booleanValue()) { + // If it never worked, remove it + supportedMechanisms.remove(mechanism); + } + } + } + + /** + * Resolves an {@link InetSocketAddress}. + * + * @param remote + * to resolve + * @return the resolved {@link InetAddress}, or {@code null} if unresolved. + */ + public static InetAddress resolve(@NonNull InetSocketAddress remote) { + InetAddress address = remote.getAddress(); + if (address == null) { + try { + address = InetAddress.getByName(remote.getHostString()); + } catch (UnknownHostException e) { + return null; + } + } + return address; + } + + /** + * Determines a canonical host name for use use with GSS-API. + * + * @param remote + * to get the host name from + * @return the canonical host name, if it can be determined, otherwise the + * {@link InetSocketAddress#getHostString() unprocessed host name}. + */ + @NonNull + public static String getCanonicalName(@NonNull InetSocketAddress remote) { + InetAddress address = resolve(remote); + if (address == null) { + return remote.getHostString(); + } + return address.getCanonicalHostName(); + } + + /** + * Creates a {@link GSSContext} for the given mechanism to authenticate with + * the host given by {@code fqdn}. + * + * @param mechanism + * {@link Oid} of the mechanism to use + * @param fqdn + * fully qualified domain name of the host to authenticate with + * @return the context, if the mechanism is available and the context could + * be created, or {@code null} otherwise + */ + public static GSSContext createContext(@NonNull Oid mechanism, + @NonNull String fqdn) { + GSSContext context = null; + try { + GSSManager manager = GSSManager.getInstance(); + context = manager.createContext( + manager.createName( + GssApiMechanisms.GSSAPI_HOST_PREFIX + fqdn, + GSSName.NT_HOSTBASED_SERVICE), + mechanism, null, GSSContext.DEFAULT_LIFETIME); + } catch (GSSException e) { + closeContextSilently(context); + failed(mechanism); + return null; + } + worked(mechanism); + return context; + } + + /** + * Closes (disposes of) a {@link GSSContext} ignoring any + * {@link GSSException}s. + * + * @param context + * to dispose + */ + public static void closeContextSilently(GSSContext context) { + if (context != null) { + try { + context.dispose(); + } catch (GSSException e) { + // Ignore + } + } + } + + private static Oid createOid(String rep) { + try { + return new Oid(rep); + } catch (GSSException e) { + // Does not occur + return null; + } + } + +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthFactory.java new file mode 100644 index 000000000..ba5630516 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import org.apache.sshd.client.auth.AbstractUserAuthFactory; +import org.apache.sshd.client.auth.UserAuth; + +/** + * Factory to create {@link GssApiWithMicAuthentication} handlers. + */ +public class GssApiWithMicAuthFactory extends AbstractUserAuthFactory { + + /** The authentication identifier for GSSApi-with-MIC. */ + public static final String NAME = "gssapi-with-mic"; //$NON-NLS-1$ + + /** The singleton {@link GssApiWithMicAuthFactory}. */ + public static final GssApiWithMicAuthFactory INSTANCE = new GssApiWithMicAuthFactory(); + + private GssApiWithMicAuthFactory() { + super(NAME); + } + + @Override + public UserAuth create() { + return new GssApiWithMicAuthentication(); + } + +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java new file mode 100644 index 000000000..fe6671489 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2018, Thomas Wolf + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.sshd; + +import static java.text.MessageFormat.format; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Iterator; + +import org.apache.sshd.client.auth.AbstractUserAuth; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.MessageProp; +import org.ietf.jgss.Oid; + +/** + * GSSAPI-with-MIC authentication handler (Kerberos 5). + * + * @see RFC 4462 + */ +public class GssApiWithMicAuthentication extends AbstractUserAuth { + + /** Synonym used in RFC 4462. */ + private static final byte SSH_MSG_USERAUTH_GSSAPI_RESPONSE = SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST; + + /** Synonym used in RFC 4462. */ + private static final byte SSH_MSG_USERAUTH_GSSAPI_TOKEN = SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE; + + private enum ProtocolState { + STARTED, TOKENS, MIC_SENT, FAILED + } + + private Collection mechanisms; + + private Iterator nextMechanism; + + private Oid currentMechanism; + + private ProtocolState state; + + private GSSContext context; + + /** Creates a new {@link GssApiWithMicAuthentication}. */ + public GssApiWithMicAuthentication() { + super(GssApiWithMicAuthFactory.NAME); + } + + @Override + protected boolean sendAuthDataRequest(ClientSession session, String service) + throws Exception { + if (mechanisms == null) { + mechanisms = GssApiMechanisms.getSupportedMechanisms(); + nextMechanism = mechanisms.iterator(); + } + if (context != null) { + close(false); + } + if (!nextMechanism.hasNext()) { + return false; + } + state = ProtocolState.STARTED; + currentMechanism = nextMechanism.next(); + try { + String hostName = getHostName(session); + context = GssApiMechanisms.createContext(currentMechanism, + hostName); + context.requestMutualAuth(true); + context.requestConf(true); + context.requestInteg(true); + context.requestCredDeleg(true); + context.requestAnonymity(false); + } catch (GSSException | NullPointerException e) { + close(true); + if (log.isDebugEnabled()) { + log.debug(format(SshdText.get().gssapiInitFailure, + currentMechanism.toString())); + } + currentMechanism = null; + state = ProtocolState.FAILED; + return false; + } + Buffer buffer = session + .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST); + buffer.putString(session.getUsername()); + buffer.putString(service); + buffer.putString(getName()); + buffer.putInt(1); + buffer.putBytes(currentMechanism.getDER()); + session.writePacket(buffer); + return true; + } + + @Override + protected boolean processAuthDataRequest(ClientSession session, + String service, Buffer in) throws Exception { + // SSH_MSG_USERAUTH_FAILURE and SSH_MSG_USERAUTH_SUCCESS, as well as + // SSH_MSG_USERAUTH_BANNER are handled by the framework. + int command = in.getUByte(); + if (context == null) { + return false; + } + try { + switch (command) { + case SSH_MSG_USERAUTH_GSSAPI_RESPONSE: { + if (state != ProtocolState.STARTED) { + return unexpectedMessage(command); + } + // Initial reply from the server with the mechanism to use. + Oid mechanism = new Oid(in.getBytes()); + if (!currentMechanism.equals(mechanism)) { + return false; + } + replyToken(session, service, new byte[0]); + return true; + } + case SSH_MSG_USERAUTH_GSSAPI_TOKEN: { + if (context.isEstablished() || state != ProtocolState.TOKENS) { + return unexpectedMessage(command); + } + // Server sent us a token + replyToken(session, service, in.getBytes()); + return true; + } + default: + return unexpectedMessage(command); + } + } catch (GSSException e) { + log.warn(format(SshdText.get().gssapiFailure, + currentMechanism.toString()), e); + state = ProtocolState.FAILED; + return false; + } + } + + @Override + public void destroy() { + try { + close(false); + } finally { + super.destroy(); + } + } + + private void close(boolean silent) { + try { + if (context != null) { + context.dispose(); + context = null; + } + } catch (GSSException e) { + if (!silent) { + log.warn(SshdText.get().gssapiFailure, e); + } + } + } + + private void sendToken(ClientSession session, byte[] receivedToken) + throws IOException, GSSException { + state = ProtocolState.TOKENS; + byte[] token = context.initSecContext(receivedToken, 0, + receivedToken.length); + if (token != null) { + Buffer buffer = session.createBuffer(SSH_MSG_USERAUTH_GSSAPI_TOKEN); + buffer.putBytes(token); + session.writePacket(buffer); + } + } + + private void sendMic(ClientSession session, String service) + throws IOException, GSSException { + state = ProtocolState.MIC_SENT; + // Produce MIC + Buffer micBuffer = new ByteArrayBuffer(); + micBuffer.putBytes(session.getSessionId()); + micBuffer.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST); + micBuffer.putString(session.getUsername()); + micBuffer.putString(service); + micBuffer.putString(getName()); + byte[] micBytes = micBuffer.getCompactData(); + byte[] mic = context.getMIC(micBytes, 0, micBytes.length, + new MessageProp(0, true)); + Buffer buffer = session + .createBuffer(SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC); + buffer.putBytes(mic); + session.writePacket(buffer); + } + + private void replyToken(ClientSession session, String service, byte[] bytes) + throws IOException, GSSException { + sendToken(session, bytes); + if (context.isEstablished()) { + sendMic(session, service); + } + } + + private String getHostName(ClientSession session) { + SocketAddress remote = session.getConnectAddress(); + if (remote instanceof InetSocketAddress) { + InetAddress address = GssApiMechanisms + .resolve((InetSocketAddress) remote); + if (address != null) { + return address.getCanonicalHostName(); + } + } + if (session instanceof JGitClientSession) { + String hostName = ((JGitClientSession) session).getHostConfigEntry() + .getHostName(); + try { + hostName = InetAddress.getByName(hostName) + .getCanonicalHostName(); + } catch (UnknownHostException e) { + // Ignore here; try with the non-canonical name + } + return hostName; + } + throw new IllegalStateException( + "Wrong session class :" + session.getClass().getName()); //$NON-NLS-1$ + } + + private boolean unexpectedMessage(int command) { + log.warn(format(SshdText.get().gssapiUnexpectedMessage, getName(), + Integer.toString(command))); + return false; + } + +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 7f08f72f2..75f884236 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -22,6 +22,10 @@ public static SshdText get() { /***/ public String closeListenerFailed; /***/ public String configInvalidPath; /***/ public String ftpCloseFailed; + /***/ public String gssapiFailure; + /***/ public String gssapiInitFailure; + /***/ public String gssapiUnexpectedMechanism; + /***/ public String gssapiUnexpectedMessage; /***/ public String keyEncryptedMsg; /***/ public String keyEncryptedPrompt; /***/ public String keyLoadFailed; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index 62fa6afdf..08d08090e 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -74,6 +74,7 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; +import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory; import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; @@ -427,15 +428,19 @@ protected FilePasswordProvider createFilePasswordProvider( /** * Gets the user authentication mechanisms (or rather, factories for them). - * By default this returns public-key, keyboard-interactive, and password, - * in that order. (I.e., we don't do gssapi-with-mic or hostbased (yet)). + * By default this returns gssapi-with-mic, public-key, + * keyboard-interactive, and password, in that order. The order is only + * significant if the ssh config does not set + * {@code PreferredAuthentications}; if it is set, the order defined there + * will be taken. * * @return the non-empty list of factories. */ @NonNull protected List> getUserAuthFactories() { return Collections.unmodifiableList( - Arrays.asList(JGitPublicKeyAuthFactory.INSTANCE, + Arrays.asList(GssApiWithMicAuthFactory.INSTANCE, + JGitPublicKeyAuthFactory.INSTANCE, UserAuthKeyboardInteractiveFactory.INSTANCE, UserAuthPasswordFactory.INSTANCE)); }