/* * 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(); // 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; } }