View Javadoc
1   /*
2    * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.internal.transport.sshd;
44  
45  import static java.text.MessageFormat.format;
46  
47  import java.io.IOException;
48  import java.net.InetAddress;
49  import java.net.InetSocketAddress;
50  import java.net.SocketAddress;
51  import java.net.UnknownHostException;
52  import java.util.Collection;
53  import java.util.Iterator;
54  
55  import org.apache.sshd.client.auth.AbstractUserAuth;
56  import org.apache.sshd.client.session.ClientSession;
57  import org.apache.sshd.common.SshConstants;
58  import org.apache.sshd.common.util.buffer.Buffer;
59  import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
60  import org.ietf.jgss.GSSContext;
61  import org.ietf.jgss.GSSException;
62  import org.ietf.jgss.MessageProp;
63  import org.ietf.jgss.Oid;
64  
65  /**
66   * GSSAPI-with-MIC authentication handler (Kerberos 5).
67   *
68   * @see <a href="https://tools.ietf.org/html/rfc4462">RFC 4462</a>
69   */
70  public class GssApiWithMicAuthentication extends AbstractUserAuth {
71  
72  	/** Synonym used in RFC 4462. */
73  	private static final byte SSH_MSG_USERAUTH_GSSAPI_RESPONSE = SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;
74  
75  	/** Synonym used in RFC 4462. */
76  	private static final byte SSH_MSG_USERAUTH_GSSAPI_TOKEN = SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;
77  
78  	private enum ProtocolState {
79  		STARTED, TOKENS, MIC_SENT, FAILED
80  	}
81  
82  	private Collection<Oid> mechanisms;
83  
84  	private Iterator<Oid> nextMechanism;
85  
86  	private Oid currentMechanism;
87  
88  	private ProtocolState state;
89  
90  	private GSSContext context;
91  
92  	/** Creates a new {@link GssApiWithMicAuthentication}. */
93  	public GssApiWithMicAuthentication() {
94  		super(GssApiWithMicAuthFactory.NAME);
95  	}
96  
97  	@Override
98  	protected boolean sendAuthDataRequest(ClientSession session, String service)
99  			throws Exception {
100 		if (mechanisms == null) {
101 			mechanisms = GssApiMechanisms.getSupportedMechanisms();
102 			nextMechanism = mechanisms.iterator();
103 		}
104 		if (context != null) {
105 			close(false);
106 		}
107 		if (!nextMechanism.hasNext()) {
108 			return false;
109 		}
110 		state = ProtocolState.STARTED;
111 		currentMechanism = nextMechanism.next();
112 		// RFC 4462 states that SPNEGO must not be used with ssh
113 		while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
114 			if (!nextMechanism.hasNext()) {
115 				return false;
116 			}
117 			currentMechanism = nextMechanism.next();
118 		}
119 		try {
120 			String hostName = getHostName(session);
121 			context = GssApiMechanisms.createContext(currentMechanism,
122 					hostName);
123 			context.requestMutualAuth(true);
124 			context.requestConf(true);
125 			context.requestInteg(true);
126 			context.requestCredDeleg(true);
127 			context.requestAnonymity(false);
128 		} catch (GSSException | NullPointerException e) {
129 			close(true);
130 			if (log.isDebugEnabled()) {
131 				log.debug(format(SshdText.get().gssapiInitFailure,
132 						currentMechanism.toString()));
133 			}
134 			currentMechanism = null;
135 			state = ProtocolState.FAILED;
136 			return false;
137 		}
138 		Buffer buffer = session
139 				.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
140 		buffer.putString(session.getUsername());
141 		buffer.putString(service);
142 		buffer.putString(getName());
143 		buffer.putInt(1);
144 		buffer.putBytes(currentMechanism.getDER());
145 		session.writePacket(buffer);
146 		return true;
147 	}
148 
149 	@Override
150 	protected boolean processAuthDataRequest(ClientSession session,
151 			String service, Buffer in) throws Exception {
152 		// SSH_MSG_USERAUTH_FAILURE and SSH_MSG_USERAUTH_SUCCESS, as well as
153 		// SSH_MSG_USERAUTH_BANNER are handled by the framework.
154 		int command = in.getUByte();
155 		if (context == null) {
156 			return false;
157 		}
158 		try {
159 			switch (command) {
160 			case SSH_MSG_USERAUTH_GSSAPI_RESPONSE: {
161 				if (state != ProtocolState.STARTED) {
162 					return unexpectedMessage(command);
163 				}
164 				// Initial reply from the server with the mechanism to use.
165 				Oid mechanism = new Oid(in.getBytes());
166 				if (!currentMechanism.equals(mechanism)) {
167 					return false;
168 				}
169 				replyToken(session, service, new byte[0]);
170 				return true;
171 			}
172 			case SSH_MSG_USERAUTH_GSSAPI_TOKEN: {
173 				if (context.isEstablished() || state != ProtocolState.TOKENS) {
174 					return unexpectedMessage(command);
175 				}
176 				// Server sent us a token
177 				replyToken(session, service, in.getBytes());
178 				return true;
179 			}
180 			default:
181 				return unexpectedMessage(command);
182 			}
183 		} catch (GSSException e) {
184 			log.warn(format(SshdText.get().gssapiFailure,
185 					currentMechanism.toString()), e);
186 			state = ProtocolState.FAILED;
187 			return false;
188 		}
189 	}
190 
191 	@Override
192 	public void destroy() {
193 		try {
194 			close(false);
195 		} finally {
196 			super.destroy();
197 		}
198 	}
199 
200 	private void close(boolean silent) {
201 		try {
202 			if (context != null) {
203 				context.dispose();
204 				context = null;
205 			}
206 		} catch (GSSException e) {
207 			if (!silent) {
208 				log.warn(SshdText.get().gssapiFailure, e);
209 			}
210 		}
211 	}
212 
213 	private void sendToken(ClientSession session, byte[] receivedToken)
214 			throws IOException, GSSException {
215 		state = ProtocolState.TOKENS;
216 		byte[] token = context.initSecContext(receivedToken, 0,
217 				receivedToken.length);
218 		if (token != null) {
219 			Buffer buffer = session.createBuffer(SSH_MSG_USERAUTH_GSSAPI_TOKEN);
220 			buffer.putBytes(token);
221 			session.writePacket(buffer);
222 		}
223 	}
224 
225 	private void sendMic(ClientSession session, String service)
226 			throws IOException, GSSException {
227 		state = ProtocolState.MIC_SENT;
228 		// Produce MIC
229 		Buffer micBuffer = new ByteArrayBuffer();
230 		micBuffer.putBytes(session.getSessionId());
231 		micBuffer.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST);
232 		micBuffer.putString(session.getUsername());
233 		micBuffer.putString(service);
234 		micBuffer.putString(getName());
235 		byte[] micBytes = micBuffer.getCompactData();
236 		byte[] mic = context.getMIC(micBytes, 0, micBytes.length,
237 				new MessageProp(0, true));
238 		Buffer buffer = session
239 				.createBuffer(SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC);
240 		buffer.putBytes(mic);
241 		session.writePacket(buffer);
242 	}
243 
244 	private void replyToken(ClientSession session, String service, byte[] bytes)
245 			throws IOException, GSSException {
246 		sendToken(session, bytes);
247 		if (context.isEstablished()) {
248 			sendMic(session, service);
249 		}
250 	}
251 
252 	private String getHostName(ClientSession session) {
253 		SocketAddress remote = session.getConnectAddress();
254 		if (remote instanceof InetSocketAddress) {
255 			InetAddress address = GssApiMechanisms
256 					.resolve((InetSocketAddress) remote);
257 			if (address != null) {
258 				return address.getCanonicalHostName();
259 			}
260 		}
261 		if (session instanceof JGitClientSession) {
262 			String hostName = ((JGitClientSession) session).getHostConfigEntry()
263 					.getHostName();
264 			try {
265 				hostName = InetAddress.getByName(hostName)
266 						.getCanonicalHostName();
267 			} catch (UnknownHostException e) {
268 				// Ignore here; try with the non-canonical name
269 			}
270 			return hostName;
271 		}
272 		throw new IllegalStateException(
273 				"Wrong session class :" + session.getClass().getName()); //$NON-NLS-1$
274 	}
275 
276 	private boolean unexpectedMessage(int command) {
277 		log.warn(format(SshdText.get().gssapiUnexpectedMessage, getName(),
278 				Integer.toString(command)));
279 		return false;
280 	}
281 
282 }