summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.junit/META-INF/MANIFEST.MF7
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java55
-rw-r--r--org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties4
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java234
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthFactory.java68
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java275
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java4
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java11
8 files changed, 653 insertions, 5 deletions
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index e44ee0301e..044576fcc8 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 8d3207c43e..3c1111d242 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.GeneralSecurityException;
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 @@ public class SshTestGitServer {
.getParentFile().getAbsoluteFile().toPath();
}
});
+ server.setUserAuthFactories(getAuthFactories());
server.setSubsystemFactories(Collections
.singletonList((new SftpSubsystemFactory.Builder()).build()));
// No shell
@@ -149,8 +161,15 @@ public class SshTestGitServer {
// 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 @@ public class SshTestGitServer {
});
}
+ 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<NamedFactory<UserAuth>> getAuthFactories() {
+ List<NamedFactory<UserAuth>> 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 72bca6a975..963e3d95fa 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 0000000000..834a503096
--- /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 <thomas.wolf@paranor.ch>
+ * 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<Oid, Boolean> supportedMechanisms;
+
+ /**
+ * Retrieves an immutable collection of the supported mechanisms.
+ *
+ * @return the supported mechanisms
+ */
+ @NonNull
+ public static Collection<Oid> getSupportedMechanisms() {
+ synchronized (LOCK) {
+ if (supportedMechanisms == null) {
+ GSSManager manager = GSSManager.getInstance();
+ Oid[] mechs = manager.getMechs();
+ Map<Oid, Boolean> 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 0000000000..ba5630516f
--- /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 <thomas.wolf@paranor.ch>
+ * 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 0000000000..fe6671489c
--- /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 <thomas.wolf@paranor.ch>
+ * 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 <a href="https://tools.ietf.org/html/rfc4462">RFC 4462</a>
+ */
+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<Oid> mechanisms;
+
+ private Iterator<Oid> 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 7f08f72f25..75f8842361 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 final class SshdText extends TranslationBundle {
/***/ 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 62fa6afdf9..08d08090e6 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.apache.sshd.common.keyprovider.KeyPairProvider;
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 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
/**
* 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 <em>not</em> set
+ * {@code PreferredAuthentications}; if it is set, the order defined there
+ * will be taken.
*
* @return the non-empty list of factories.
*/
@NonNull
protected List<NamedFactory<UserAuth>> getUserAuthFactories() {
return Collections.unmodifiableList(
- Arrays.asList(JGitPublicKeyAuthFactory.INSTANCE,
+ Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
+ JGitPublicKeyAuthFactory.INSTANCE,
UserAuthKeyboardInteractiveFactory.INSTANCE,
UserAuthPasswordFactory.INSTANCE));
}