GssApiWithMicAuthentication.java

  1. /*
  2.  * Copyright (C) 2018, 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. import static java.text.MessageFormat.format;

  12. import java.io.IOException;
  13. import java.net.InetAddress;
  14. import java.net.InetSocketAddress;
  15. import java.net.SocketAddress;
  16. import java.net.UnknownHostException;
  17. import java.util.Collection;
  18. import java.util.Iterator;

  19. import org.apache.sshd.client.auth.AbstractUserAuth;
  20. import org.apache.sshd.client.session.ClientSession;
  21. import org.apache.sshd.common.SshConstants;
  22. import org.apache.sshd.common.util.buffer.Buffer;
  23. import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  24. import org.ietf.jgss.GSSContext;
  25. import org.ietf.jgss.GSSException;
  26. import org.ietf.jgss.MessageProp;
  27. import org.ietf.jgss.Oid;

  28. /**
  29.  * GSSAPI-with-MIC authentication handler (Kerberos 5).
  30.  *
  31.  * @see <a href="https://tools.ietf.org/html/rfc4462">RFC 4462</a>
  32.  */
  33. public class GssApiWithMicAuthentication extends AbstractUserAuth {

  34.     /** Synonym used in RFC 4462. */
  35.     private static final byte SSH_MSG_USERAUTH_GSSAPI_RESPONSE = SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST;

  36.     /** Synonym used in RFC 4462. */
  37.     private static final byte SSH_MSG_USERAUTH_GSSAPI_TOKEN = SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE;

  38.     private enum ProtocolState {
  39.         STARTED, TOKENS, MIC_SENT, FAILED
  40.     }

  41.     private Collection<Oid> mechanisms;

  42.     private Iterator<Oid> nextMechanism;

  43.     private Oid currentMechanism;

  44.     private ProtocolState state;

  45.     private GSSContext context;

  46.     /** Creates a new {@link GssApiWithMicAuthentication}. */
  47.     public GssApiWithMicAuthentication() {
  48.         super(GssApiWithMicAuthFactory.NAME);
  49.     }

  50.     @Override
  51.     protected boolean sendAuthDataRequest(ClientSession session, String service)
  52.             throws Exception {
  53.         if (mechanisms == null) {
  54.             mechanisms = GssApiMechanisms.getSupportedMechanisms();
  55.             nextMechanism = mechanisms.iterator();
  56.         }
  57.         if (context != null) {
  58.             close(false);
  59.         }
  60.         if (!nextMechanism.hasNext()) {
  61.             return false;
  62.         }
  63.         state = ProtocolState.STARTED;
  64.         currentMechanism = nextMechanism.next();
  65.         // RFC 4462 states that SPNEGO must not be used with ssh
  66.         while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
  67.             if (!nextMechanism.hasNext()) {
  68.                 return false;
  69.             }
  70.             currentMechanism = nextMechanism.next();
  71.         }
  72.         try {
  73.             String hostName = getHostName(session);
  74.             context = GssApiMechanisms.createContext(currentMechanism,
  75.                     hostName);
  76.             context.requestMutualAuth(true);
  77.             context.requestConf(true);
  78.             context.requestInteg(true);
  79.             context.requestCredDeleg(true);
  80.             context.requestAnonymity(false);
  81.         } catch (GSSException | NullPointerException e) {
  82.             close(true);
  83.             if (log.isDebugEnabled()) {
  84.                 log.debug(format(SshdText.get().gssapiInitFailure,
  85.                         currentMechanism.toString()));
  86.             }
  87.             currentMechanism = null;
  88.             state = ProtocolState.FAILED;
  89.             return false;
  90.         }
  91.         Buffer buffer = session
  92.                 .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
  93.         buffer.putString(session.getUsername());
  94.         buffer.putString(service);
  95.         buffer.putString(getName());
  96.         buffer.putInt(1);
  97.         buffer.putBytes(currentMechanism.getDER());
  98.         session.writePacket(buffer);
  99.         return true;
  100.     }

  101.     @Override
  102.     protected boolean processAuthDataRequest(ClientSession session,
  103.             String service, Buffer in) throws Exception {
  104.         // SSH_MSG_USERAUTH_FAILURE and SSH_MSG_USERAUTH_SUCCESS, as well as
  105.         // SSH_MSG_USERAUTH_BANNER are handled by the framework.
  106.         int command = in.getUByte();
  107.         if (context == null) {
  108.             return false;
  109.         }
  110.         try {
  111.             switch (command) {
  112.             case SSH_MSG_USERAUTH_GSSAPI_RESPONSE: {
  113.                 if (state != ProtocolState.STARTED) {
  114.                     return unexpectedMessage(command);
  115.                 }
  116.                 // Initial reply from the server with the mechanism to use.
  117.                 Oid mechanism = new Oid(in.getBytes());
  118.                 if (!currentMechanism.equals(mechanism)) {
  119.                     return false;
  120.                 }
  121.                 replyToken(session, service, new byte[0]);
  122.                 return true;
  123.             }
  124.             case SSH_MSG_USERAUTH_GSSAPI_TOKEN: {
  125.                 if (context.isEstablished() || state != ProtocolState.TOKENS) {
  126.                     return unexpectedMessage(command);
  127.                 }
  128.                 // Server sent us a token
  129.                 replyToken(session, service, in.getBytes());
  130.                 return true;
  131.             }
  132.             default:
  133.                 return unexpectedMessage(command);
  134.             }
  135.         } catch (GSSException e) {
  136.             log.warn(format(SshdText.get().gssapiFailure,
  137.                     currentMechanism.toString()), e);
  138.             state = ProtocolState.FAILED;
  139.             return false;
  140.         }
  141.     }

  142.     @Override
  143.     public void destroy() {
  144.         try {
  145.             close(false);
  146.         } finally {
  147.             super.destroy();
  148.         }
  149.     }

  150.     private void close(boolean silent) {
  151.         try {
  152.             if (context != null) {
  153.                 context.dispose();
  154.                 context = null;
  155.             }
  156.         } catch (GSSException e) {
  157.             if (!silent) {
  158.                 log.warn(SshdText.get().gssapiFailure, e);
  159.             }
  160.         }
  161.     }

  162.     private void sendToken(ClientSession session, byte[] receivedToken)
  163.             throws IOException, GSSException {
  164.         state = ProtocolState.TOKENS;
  165.         byte[] token = context.initSecContext(receivedToken, 0,
  166.                 receivedToken.length);
  167.         if (token != null) {
  168.             Buffer buffer = session.createBuffer(SSH_MSG_USERAUTH_GSSAPI_TOKEN);
  169.             buffer.putBytes(token);
  170.             session.writePacket(buffer);
  171.         }
  172.     }

  173.     private void sendMic(ClientSession session, String service)
  174.             throws IOException, GSSException {
  175.         state = ProtocolState.MIC_SENT;
  176.         // Produce MIC
  177.         Buffer micBuffer = new ByteArrayBuffer();
  178.         micBuffer.putBytes(session.getSessionId());
  179.         micBuffer.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST);
  180.         micBuffer.putString(session.getUsername());
  181.         micBuffer.putString(service);
  182.         micBuffer.putString(getName());
  183.         byte[] micBytes = micBuffer.getCompactData();
  184.         byte[] mic = context.getMIC(micBytes, 0, micBytes.length,
  185.                 new MessageProp(0, true));
  186.         Buffer buffer = session
  187.                 .createBuffer(SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC);
  188.         buffer.putBytes(mic);
  189.         session.writePacket(buffer);
  190.     }

  191.     private void replyToken(ClientSession session, String service, byte[] bytes)
  192.             throws IOException, GSSException {
  193.         sendToken(session, bytes);
  194.         if (context.isEstablished()) {
  195.             sendMic(session, service);
  196.         }
  197.     }

  198.     private String getHostName(ClientSession session) {
  199.         SocketAddress remote = session.getConnectAddress();
  200.         if (remote instanceof InetSocketAddress) {
  201.             InetAddress address = GssApiMechanisms
  202.                     .resolve((InetSocketAddress) remote);
  203.             if (address != null) {
  204.                 return address.getCanonicalHostName();
  205.             }
  206.         }
  207.         if (session instanceof JGitClientSession) {
  208.             String hostName = ((JGitClientSession) session).getHostConfigEntry()
  209.                     .getHostName();
  210.             try {
  211.                 hostName = InetAddress.getByName(hostName)
  212.                         .getCanonicalHostName();
  213.             } catch (UnknownHostException e) {
  214.                 // Ignore here; try with the non-canonical name
  215.             }
  216.             return hostName;
  217.         }
  218.         throw new IllegalStateException(
  219.                 "Wrong session class :" + session.getClass().getName()); //$NON-NLS-1$
  220.     }

  221.     private boolean unexpectedMessage(int command) {
  222.         log.warn(format(SshdText.get().gssapiUnexpectedMessage, getName(),
  223.                 Integer.toString(command)));
  224.         return false;
  225.     }

  226. }