| /* |
| * 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(); |
| // RFC 4462 states that SPNEGO must not be used with ssh |
| while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) { |
| if (!nextMechanism.hasNext()) { |
| return false; |
| } |
| 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; |
| } |
| |
| } |