You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GssApiWithMicAuthentication.java 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. import static java.text.MessageFormat.format;
  45. import java.io.IOException;
  46. import java.net.InetAddress;
  47. import java.net.InetSocketAddress;
  48. import java.net.SocketAddress;
  49. import java.net.UnknownHostException;
  50. import java.util.Collection;
  51. import java.util.Iterator;
  52. import org.apache.sshd.client.auth.AbstractUserAuth;
  53. import org.apache.sshd.client.session.ClientSession;
  54. import org.apache.sshd.common.SshConstants;
  55. import org.apache.sshd.common.util.buffer.Buffer;
  56. import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  57. import org.ietf.jgss.GSSContext;
  58. import org.ietf.jgss.GSSException;
  59. import org.ietf.jgss.MessageProp;
  60. import org.ietf.jgss.Oid;
  61. /**
  62. * GSSAPI-with-MIC authentication handler (Kerberos 5).
  63. *
  64. * @see <a href="https://tools.ietf.org/html/rfc4462">RFC 4462</a>
  65. */
  66. public class GssApiWithMicAuthentication extends AbstractUserAuth {
  67. /** Synonym used in RFC 4462. */
  68. private static final byte SSH_MSG_USERAUTH_GSSAPI_RESPONSE = SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;
  69. /** Synonym used in RFC 4462. */
  70. private static final byte SSH_MSG_USERAUTH_GSSAPI_TOKEN = SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;
  71. private enum ProtocolState {
  72. STARTED, TOKENS, MIC_SENT, FAILED
  73. }
  74. private Collection<Oid> mechanisms;
  75. private Iterator<Oid> nextMechanism;
  76. private Oid currentMechanism;
  77. private ProtocolState state;
  78. private GSSContext context;
  79. /** Creates a new {@link GssApiWithMicAuthentication}. */
  80. public GssApiWithMicAuthentication() {
  81. super(GssApiWithMicAuthFactory.NAME);
  82. }
  83. @Override
  84. protected boolean sendAuthDataRequest(ClientSession session, String service)
  85. throws Exception {
  86. if (mechanisms == null) {
  87. mechanisms = GssApiMechanisms.getSupportedMechanisms();
  88. nextMechanism = mechanisms.iterator();
  89. }
  90. if (context != null) {
  91. close(false);
  92. }
  93. if (!nextMechanism.hasNext()) {
  94. return false;
  95. }
  96. state = ProtocolState.STARTED;
  97. currentMechanism = nextMechanism.next();
  98. // RFC 4462 states that SPNEGO must not be used with ssh
  99. while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
  100. if (!nextMechanism.hasNext()) {
  101. return false;
  102. }
  103. currentMechanism = nextMechanism.next();
  104. }
  105. try {
  106. String hostName = getHostName(session);
  107. context = GssApiMechanisms.createContext(currentMechanism,
  108. hostName);
  109. context.requestMutualAuth(true);
  110. context.requestConf(true);
  111. context.requestInteg(true);
  112. context.requestCredDeleg(true);
  113. context.requestAnonymity(false);
  114. } catch (GSSException | NullPointerException e) {
  115. close(true);
  116. if (log.isDebugEnabled()) {
  117. log.debug(format(SshdText.get().gssapiInitFailure,
  118. currentMechanism.toString()));
  119. }
  120. currentMechanism = null;
  121. state = ProtocolState.FAILED;
  122. return false;
  123. }
  124. Buffer buffer = session
  125. .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
  126. buffer.putString(session.getUsername());
  127. buffer.putString(service);
  128. buffer.putString(getName());
  129. buffer.putInt(1);
  130. buffer.putBytes(currentMechanism.getDER());
  131. session.writePacket(buffer);
  132. return true;
  133. }
  134. @Override
  135. protected boolean processAuthDataRequest(ClientSession session,
  136. String service, Buffer in) throws Exception {
  137. // SSH_MSG_USERAUTH_FAILURE and SSH_MSG_USERAUTH_SUCCESS, as well as
  138. // SSH_MSG_USERAUTH_BANNER are handled by the framework.
  139. int command = in.getUByte();
  140. if (context == null) {
  141. return false;
  142. }
  143. try {
  144. switch (command) {
  145. case SSH_MSG_USERAUTH_GSSAPI_RESPONSE: {
  146. if (state != ProtocolState.STARTED) {
  147. return unexpectedMessage(command);
  148. }
  149. // Initial reply from the server with the mechanism to use.
  150. Oid mechanism = new Oid(in.getBytes());
  151. if (!currentMechanism.equals(mechanism)) {
  152. return false;
  153. }
  154. replyToken(session, service, new byte[0]);
  155. return true;
  156. }
  157. case SSH_MSG_USERAUTH_GSSAPI_TOKEN: {
  158. if (context.isEstablished() || state != ProtocolState.TOKENS) {
  159. return unexpectedMessage(command);
  160. }
  161. // Server sent us a token
  162. replyToken(session, service, in.getBytes());
  163. return true;
  164. }
  165. default:
  166. return unexpectedMessage(command);
  167. }
  168. } catch (GSSException e) {
  169. log.warn(format(SshdText.get().gssapiFailure,
  170. currentMechanism.toString()), e);
  171. state = ProtocolState.FAILED;
  172. return false;
  173. }
  174. }
  175. @Override
  176. public void destroy() {
  177. try {
  178. close(false);
  179. } finally {
  180. super.destroy();
  181. }
  182. }
  183. private void close(boolean silent) {
  184. try {
  185. if (context != null) {
  186. context.dispose();
  187. context = null;
  188. }
  189. } catch (GSSException e) {
  190. if (!silent) {
  191. log.warn(SshdText.get().gssapiFailure, e);
  192. }
  193. }
  194. }
  195. private void sendToken(ClientSession session, byte[] receivedToken)
  196. throws IOException, GSSException {
  197. state = ProtocolState.TOKENS;
  198. byte[] token = context.initSecContext(receivedToken, 0,
  199. receivedToken.length);
  200. if (token != null) {
  201. Buffer buffer = session.createBuffer(SSH_MSG_USERAUTH_GSSAPI_TOKEN);
  202. buffer.putBytes(token);
  203. session.writePacket(buffer);
  204. }
  205. }
  206. private void sendMic(ClientSession session, String service)
  207. throws IOException, GSSException {
  208. state = ProtocolState.MIC_SENT;
  209. // Produce MIC
  210. Buffer micBuffer = new ByteArrayBuffer();
  211. micBuffer.putBytes(session.getSessionId());
  212. micBuffer.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST);
  213. micBuffer.putString(session.getUsername());
  214. micBuffer.putString(service);
  215. micBuffer.putString(getName());
  216. byte[] micBytes = micBuffer.getCompactData();
  217. byte[] mic = context.getMIC(micBytes, 0, micBytes.length,
  218. new MessageProp(0, true));
  219. Buffer buffer = session
  220. .createBuffer(SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC);
  221. buffer.putBytes(mic);
  222. session.writePacket(buffer);
  223. }
  224. private void replyToken(ClientSession session, String service, byte[] bytes)
  225. throws IOException, GSSException {
  226. sendToken(session, bytes);
  227. if (context.isEstablished()) {
  228. sendMic(session, service);
  229. }
  230. }
  231. private String getHostName(ClientSession session) {
  232. SocketAddress remote = session.getConnectAddress();
  233. if (remote instanceof InetSocketAddress) {
  234. InetAddress address = GssApiMechanisms
  235. .resolve((InetSocketAddress) remote);
  236. if (address != null) {
  237. return address.getCanonicalHostName();
  238. }
  239. }
  240. if (session instanceof JGitClientSession) {
  241. String hostName = ((JGitClientSession) session).getHostConfigEntry()
  242. .getHostName();
  243. try {
  244. hostName = InetAddress.getByName(hostName)
  245. .getCanonicalHostName();
  246. } catch (UnknownHostException e) {
  247. // Ignore here; try with the non-canonical name
  248. }
  249. return hostName;
  250. }
  251. throw new IllegalStateException(
  252. "Wrong session class :" + session.getClass().getName()); //$NON-NLS-1$
  253. }
  254. private boolean unexpectedMessage(int command) {
  255. log.warn(format(SshdText.get().gssapiUnexpectedMessage, getName(),
  256. Integer.toString(command)));
  257. return false;
  258. }
  259. }