| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sshd.server.auth.gss; |
| |
| import java.util.Objects; |
| |
| import org.apache.sshd.common.SshConstants; |
| import org.apache.sshd.common.SshException; |
| import org.apache.sshd.common.util.NumberUtils; |
| import org.apache.sshd.common.util.ValidateUtils; |
| import org.apache.sshd.common.util.buffer.Buffer; |
| import org.apache.sshd.common.util.buffer.ByteArrayBuffer; |
| import org.apache.sshd.server.auth.AbstractUserAuth; |
| import org.apache.sshd.server.session.ServerSession; |
| import org.ietf.jgss.GSSContext; |
| import org.ietf.jgss.GSSCredential; |
| import org.ietf.jgss.GSSException; |
| import org.ietf.jgss.GSSManager; |
| import org.ietf.jgss.GSSName; |
| import org.ietf.jgss.MessageProp; |
| import org.ietf.jgss.Oid; |
| |
| /** |
| * <p> |
| * Prototype user authentication handling gssapi-with-mic. Implements <code>HandshakingUserAuth</code> because the |
| * process involves several steps. |
| * </p> |
| * |
| * <p> |
| * Several methods are available for overriding in specific circumstances. |
| * </p> |
| */ |
| public class UserAuthGSS extends AbstractUserAuth { |
| public static final String NAME = UserAuthGSSFactory.NAME; |
| |
| // Oids for the Kerberos 5 mechanism and principal |
| public static final Oid KRB5_MECH = createOID("1.2.840.113554.1.2.2"); |
| public static final Oid KRB5_NT_PRINCIPAL = createOID("1.2.840.113554.1.2.2.1"); |
| |
| // The on-going GSS context. |
| private GSSContext context; |
| |
| // Identity from context |
| private String identity; |
| |
| public UserAuthGSS() { |
| super(NAME); |
| } |
| |
| @Override |
| protected Boolean doAuth(Buffer buffer, boolean initial) throws Exception { |
| ServerSession session = getServerSession(); |
| GSSAuthenticator auth = Objects.requireNonNull(session.getGSSAuthenticator(), "No GSSAuthenticator configured"); |
| String user = getUsername(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| |
| if (initial) { |
| // Get mechanism count from buffer and look for Kerberos 5. |
| int num = buffer.getInt(); |
| // Protect against malicious or corrupted packets |
| if ((num < 0) || (num > SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT)) { |
| log.error("doAuth({}@{}) Illogical OID entries count: {}", user, session, num); |
| throw new IndexOutOfBoundsException("Illogical OID entries count: " + num); |
| } |
| |
| boolean traceEnabled = log.isTraceEnabled(); |
| for (int i = 1; i <= num; i++) { |
| Oid oid = new Oid(buffer.getBytes()); |
| if (!oid.equals(KRB5_MECH)) { |
| if (traceEnabled) { |
| log.trace("doAuth({}@{}) skip OID {}/{}: {}", user, session, i, num, oid); |
| } |
| continue; |
| } |
| if (debugEnabled) { |
| log.debug("doAuth({}@{}) found Kerberos 5 after {}/{} OID(s)", user, session, i, num); |
| } |
| |
| // Validate initial user before proceeding |
| if (!auth.validateInitialUser(session, user)) { |
| return Boolean.FALSE; |
| } |
| |
| GSSManager mgr = auth.getGSSManager(); |
| GSSCredential creds = auth.getGSSCredential(mgr); |
| if (creds == null) { |
| return Boolean.FALSE; |
| } |
| |
| context = mgr.createContext(creds); |
| |
| // Send the matching mechanism back to the client |
| |
| byte[] out = oid.getDER(); |
| Buffer b = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST, out.length + Integer.SIZE); |
| b.putBytes(out); |
| session.writePacket(b); |
| |
| return null; |
| } |
| |
| // No matching mechanism found |
| return Boolean.FALSE; |
| } else { |
| int msg = buffer.getUByte(); |
| if (!((msg == SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE) |
| || ((msg == SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC) && context.isEstablished()))) { |
| throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Packet not supported by user authentication method: " + SshConstants.getCommandMessageName(msg)); |
| } |
| |
| if (debugEnabled) { |
| log.debug("doAuth({}@{}) In krb5.next: msg = {}", user, session, SshConstants.getCommandMessageName(msg)); |
| } |
| |
| // If the context is established, this must be a MIC message |
| |
| if (context.isEstablished()) { |
| if (msg != SshConstants.SSH_MSG_USERAUTH_GSSAPI_MIC) { |
| return Boolean.FALSE; |
| } |
| |
| // Make the MIC message so the token can be verified |
| |
| Buffer msgbuf = new ByteArrayBuffer(); |
| msgbuf.putBytes(ValidateUtils.checkNotNullAndNotEmpty(session.getSessionId(), "No current session ID")); |
| msgbuf.putByte(SshConstants.SSH_MSG_USERAUTH_REQUEST); |
| msgbuf.putString(getUsername()); |
| msgbuf.putString(getService()); |
| msgbuf.putString(getName()); |
| |
| byte[] msgbytes = msgbuf.getCompactData(); |
| byte[] inmic = buffer.getBytes(); |
| try { |
| context.verifyMIC(inmic, 0, inmic.length, msgbytes, 0, msgbytes.length, new MessageProp(false)); |
| if (debugEnabled) { |
| log.debug("doAuth({}@{}) MIC verified", getUsername(), session); |
| } |
| return Boolean.TRUE; |
| } catch (GSSException e) { |
| if (debugEnabled) { |
| log.debug("doAuth({}@{}) GSS verification {} error: {}", |
| user, session, e.getClass().getSimpleName(), e.getMessage()); |
| } |
| return Boolean.FALSE; |
| } |
| } else { |
| // Not established - new token to process |
| byte[] tok = buffer.getBytes(); |
| byte[] out = context.acceptSecContext(tok, 0, tok.length); |
| boolean established = context.isEstablished(); |
| |
| // Validate identity if context is now established |
| if (established && (identity == null)) { |
| GSSName srcName = context.getSrcName(); |
| identity = srcName.toString(); |
| if (debugEnabled) { |
| log.debug("doAuth({}@{}) GSS identity is {}", user, session, identity); |
| } |
| |
| if (!auth.validateIdentity(session, identity)) { |
| return Boolean.FALSE; |
| } |
| } |
| |
| // Send return token if necessary |
| if (NumberUtils.length(out) > 0) { |
| Buffer b = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE, out.length + Integer.SIZE); |
| b.putBytes(out); |
| session.writePacket(b); |
| return null; |
| } else { |
| return established; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Free any system resources used by the module. |
| */ |
| @Override |
| public void destroy() { |
| if (context != null) { |
| try { |
| context.dispose(); |
| } catch (GSSException e) { |
| if (log.isDebugEnabled()) { |
| log.debug("Failed ({}) to dispose of context: {}", e.getClass().getSimpleName(), e.getMessage()); |
| } |
| } finally { |
| context = null; |
| } |
| } |
| } |
| |
| /** |
| * Utility to construct an Oid from a string, ignoring the annoying exception. |
| * |
| * @param rep The string form |
| * @return The Oid |
| */ |
| public static Oid createOID(String rep) { |
| try { |
| return new Oid(rep); |
| } catch (GSSException e) { |
| // won't happen |
| return null; |
| } |
| } |
| } |