View Javadoc
1   /*
2    * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.internal.transport.sshd;
11  
12  import static java.text.MessageFormat.format;
13  
14  import java.io.IOException;
15  import java.net.InetAddress;
16  import java.net.InetSocketAddress;
17  import java.net.SocketAddress;
18  import java.net.UnknownHostException;
19  import java.util.Collection;
20  import java.util.Iterator;
21  import java.util.List;
22  
23  import org.apache.sshd.client.auth.AbstractUserAuth;
24  import org.apache.sshd.client.session.ClientSession;
25  import org.apache.sshd.common.SshConstants;
26  import org.apache.sshd.common.util.buffer.Buffer;
27  import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
28  import org.ietf.jgss.GSSContext;
29  import org.ietf.jgss.GSSException;
30  import org.ietf.jgss.MessageProp;
31  import org.ietf.jgss.Oid;
32  
33  /**
34   * GSSAPI-with-MIC authentication handler (Kerberos 5).
35   *
36   * @see <a href="https://tools.ietf.org/html/rfc4462">RFC 4462</a>
37   */
38  public class GssApiWithMicAuthentication extends AbstractUserAuth {
39  
40  	/** Synonym used in RFC 4462. */
41  	private static final byte SSH_MSG_USERAUTH_GSSAPI_RESPONSE = SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;
42  
43  	/** Synonym used in RFC 4462. */
44  	private static final byte SSH_MSG_USERAUTH_GSSAPI_TOKEN = SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;
45  
46  	private enum ProtocolState {
47  		STARTED, TOKENS, MIC_SENT, FAILED
48  	}
49  
50  	private Collection<Oid> mechanisms;
51  
52  	private Iterator<Oid> nextMechanism;
53  
54  	private Oid currentMechanism;
55  
56  	private ProtocolState state;
57  
58  	private GSSContext context;
59  
60  	/** Creates a new {@link GssApiWithMicAuthentication}. */
61  	public GssApiWithMicAuthentication() {
62  		super(GssApiWithMicAuthFactory.NAME);
63  	}
64  
65  	@Override
66  	protected boolean sendAuthDataRequest(ClientSession session, String service)
67  			throws Exception {
68  		if (mechanisms == null) {
69  			mechanisms = GssApiMechanisms.getSupportedMechanisms();
70  			nextMechanism = mechanisms.iterator();
71  		}
72  		if (context != null) {
73  			close(false);
74  		}
75  		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
76  				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
77  		if (!nextMechanism.hasNext()) {
78  			reporter.signalAuthenticationExhausted(session, service);
79  			return false;
80  		}
81  		state = ProtocolState.STARTED;
82  		currentMechanism = nextMechanism.next();
83  		// RFC 4462 states that SPNEGO must not be used with ssh
84  		while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
85  			if (!nextMechanism.hasNext()) {
86  				reporter.signalAuthenticationExhausted(session, service);
87  				return false;
88  			}
89  			currentMechanism = nextMechanism.next();
90  		}
91  		try {
92  			String hostName = getHostName(session);
93  			context = GssApiMechanisms.createContext(currentMechanism,
94  					hostName);
95  			context.requestMutualAuth(true);
96  			context.requestConf(true);
97  			context.requestInteg(true);
98  			context.requestCredDeleg(true);
99  			context.requestAnonymity(false);
100 		} catch (GSSException | NullPointerException e) {
101 			close(true);
102 			if (log.isDebugEnabled()) {
103 				log.debug(format(SshdText.get().gssapiInitFailure,
104 						currentMechanism.toString()));
105 			}
106 			currentMechanism = null;
107 			state = ProtocolState.FAILED;
108 			return false;
109 		}
110 		if (reporter != null) {
111 			reporter.signalAuthenticationAttempt(session, service,
112 					currentMechanism.toString());
113 		}
114 		Buffer buffer = session
115 				.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
116 		buffer.putString(session.getUsername());
117 		buffer.putString(service);
118 		buffer.putString(getName());
119 		buffer.putInt(1);
120 		buffer.putBytes(currentMechanism.getDER());
121 		session.writePacket(buffer);
122 		return true;
123 	}
124 
125 	@Override
126 	protected boolean processAuthDataRequest(ClientSession session,
127 			String service, Buffer in) throws Exception {
128 		// SSH_MSG_USERAUTH_FAILURE and SSH_MSG_USERAUTH_SUCCESS, as well as
129 		// SSH_MSG_USERAUTH_BANNER are handled by the framework.
130 		int command = in.getUByte();
131 		if (context == null) {
132 			return false;
133 		}
134 		try {
135 			switch (command) {
136 			case SSH_MSG_USERAUTH_GSSAPI_RESPONSE: {
137 				if (state != ProtocolState.STARTED) {
138 					return unexpectedMessage(command);
139 				}
140 				// Initial reply from the server with the mechanism to use.
141 				Oid mechanism = new Oid(in.getBytes());
142 				if (!currentMechanism.equals(mechanism)) {
143 					return false;
144 				}
145 				replyToken(session, service, new byte[0]);
146 				return true;
147 			}
148 			case SSH_MSG_USERAUTH_GSSAPI_TOKEN: {
149 				if (context.isEstablished() || state != ProtocolState.TOKENS) {
150 					return unexpectedMessage(command);
151 				}
152 				// Server sent us a token
153 				replyToken(session, service, in.getBytes());
154 				return true;
155 			}
156 			default:
157 				return unexpectedMessage(command);
158 			}
159 		} catch (GSSException e) {
160 			log.warn(format(SshdText.get().gssapiFailure,
161 					currentMechanism.toString()), e);
162 			state = ProtocolState.FAILED;
163 			return false;
164 		}
165 	}
166 
167 	@Override
168 	public void destroy() {
169 		try {
170 			close(false);
171 		} finally {
172 			super.destroy();
173 		}
174 	}
175 
176 	private void close(boolean silent) {
177 		try {
178 			if (context != null) {
179 				context.dispose();
180 				context = null;
181 			}
182 		} catch (GSSException e) {
183 			if (!silent) {
184 				log.warn(SshdText.get().gssapiFailure, e);
185 			}
186 		}
187 	}
188 
189 	private void sendToken(ClientSession session, byte[] receivedToken)
190 			throws IOException, GSSException {
191 		state = ProtocolState.TOKENS;
192 		byte[] token = context.initSecContext(receivedToken, 0,
193 				receivedToken.length);
194 		if (token != null) {
195 			Buffer buffer = session.createBuffer(SSH_MSG_USERAUTH_GSSAPI_TOKEN);
196 			buffer.putBytes(token);
197 			session.writePacket(buffer);
198 		}
199 	}
200 
201 	private void sendMic(ClientSession session, String service)
202 			throws IOException, GSSException {
203 		state = ProtocolState.MIC_SENT;
204 		// Produce MIC
205 		Buffer micBuffer = new ByteArrayBuffer();
206 		micBuffer.putBytes(session.getSessionId());
207 		micBuffer.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST);
208 		micBuffer.putString(session.getUsername());
209 		micBuffer.putString(service);
210 		micBuffer.putString(getName());
211 		byte[] micBytes = micBuffer.getCompactData();
212 		byte[] mic = context.getMIC(micBytes, 0, micBytes.length,
213 				new MessageProp(0, true));
214 		Buffer buffer = session
215 				.createBuffer(SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC);
216 		buffer.putBytes(mic);
217 		session.writePacket(buffer);
218 	}
219 
220 	private void replyToken(ClientSession session, String service, byte[] bytes)
221 			throws IOException, GSSException {
222 		sendToken(session, bytes);
223 		if (context.isEstablished()) {
224 			sendMic(session, service);
225 		}
226 	}
227 
228 	private String getHostName(ClientSession session) {
229 		SocketAddress remote = session.getConnectAddress();
230 		if (remote instanceof InetSocketAddress) {
231 			InetAddress address = GssApiMechanisms
232 					.resolve((InetSocketAddress) remote);
233 			if (address != null) {
234 				return address.getCanonicalHostName();
235 			}
236 		}
237 		if (session instanceof JGitClientSession) {
238 			String hostName = ((JGitClientSession) session).getHostConfigEntry()
239 					.getHostName();
240 			try {
241 				hostName = InetAddress.getByName(hostName)
242 						.getCanonicalHostName();
243 			} catch (UnknownHostException e) {
244 				// Ignore here; try with the non-canonical name
245 			}
246 			return hostName;
247 		}
248 		throw new IllegalStateException(
249 				"Wrong session class :" + session.getClass().getName()); //$NON-NLS-1$
250 	}
251 
252 	private boolean unexpectedMessage(int command) {
253 		log.warn(format(SshdText.get().gssapiUnexpectedMessage, getName(),
254 				Integer.toString(command)));
255 		return false;
256 	}
257 
258 	@Override
259 	public void signalAuthMethodSuccess(ClientSession session, String service,
260 			Buffer buffer) throws Exception {
261 		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
262 				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
263 		if (reporter != null) {
264 			reporter.signalAuthenticationSuccess(session, service,
265 					currentMechanism.toString());
266 		}
267 	}
268 
269 	@Override
270 	public void signalAuthMethodFailure(ClientSession session, String service,
271 			boolean partial, List<String> serverMethods, Buffer buffer)
272 			throws Exception {
273 		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
274 				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
275 		if (reporter != null) {
276 			reporter.signalAuthenticationFailure(session, service,
277 					currentMechanism.toString(), partial, serverMethods);
278 		}
279 	}
280 }