/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.wildfly.security.sasl.gssapi;
import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.GSSAPI;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
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;
import org.jboss.logging.Logger;
import org.wildfly.common.Assert;
import org.wildfly.security._private.ElytronMessages;
import org.wildfly.security.auth.callback.ServerCredentialCallback;
import org.wildfly.security.credential.GSSKerberosCredential;
/**
* SaslServer for the GSSAPI mechanism as defined by RFC 4752
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
class GssapiServer extends AbstractGssapiMechanism implements SaslServer {
private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security.sasl.gssapi.server");
private static final int ACCEPTOR_STATE = 1;
private static final int SECURITY_LAYER_ADVERTISER = 2;
private static final int SECURITY_LAYER_RECEIVER = 3;
private String authorizationId;
private byte offeredSecurityLayer;
GssapiServer(final String protocol, final String serverName, final Map<String, ?> props,
final CallbackHandler callbackHandler) throws SaslException {
super(GSSAPI, protocol, serverName, props, callbackHandler, log);
// Initialise our GSSContext
GSSManager manager = GSSManager.getInstance();
GSSContext gssContext = null;
GSSCredential ourCredential = null;
ServerCredentialCallback gssCredentialCallback = new ServerCredentialCallback(GSSKerberosCredential.class);
try {
log.trace("Obtaining GSSCredential for the service from callback handler...");
callbackHandler.handle(new Callback[] { gssCredentialCallback });
ourCredential = gssCredentialCallback.applyToCredential(GSSKerberosCredential.class, GSSKerberosCredential::getGssCredential);
} catch (IOException e) {
throw log.mechCallbackHandlerFailedForUnknownReason(GSSAPI, e).toSaslException();
} catch (UnsupportedCallbackException e) {
log.trace("Unable to obtain GSSCredential from CallbackHandler", e);
}
try {
if (ourCredential == null) {
// According to the Javadoc we will have a protocol and server name.
String localName = protocol + "@" + serverName;
log.tracef("Our name '%s'", localName);
GSSName ourName = manager.createName(localName, GSSName.NT_HOSTBASED_SERVICE, KERBEROS_V5);
ourCredential = manager.createCredential(ourName, GSSContext.INDEFINITE_LIFETIME, KERBEROS_V5,
GSSCredential.ACCEPT_ONLY);
}
gssContext = manager.createContext(ourCredential);
} catch (GSSException e) {
throw log.mechUnableToCreateGssContext(getMechanismName(), e).toSaslException();
}
// We don't request integrity or confidentiality as that is only
// supported on the client side.
this.gssContext = gssContext;
}
@Override
public void init() {
setNegotiationState(ACCEPTOR_STATE);
}
@Override
public String getAuthorizationID() {
assertComplete();
return authorizationId;
}
@Override
public byte[] evaluateResponse(byte[] response) throws SaslException {
return evaluateMessage(response);
}
@Override
protected byte[] evaluateMessage(int state, final byte[] message) throws SaslException {
switch (state) {
case ACCEPTOR_STATE:
assert gssContext.isEstablished() == false;
try {
byte[] response = gssContext.acceptSecContext(message, 0, message.length);
if (gssContext.isEstablished()) {
Oid actualMech = gssContext.getMech();
log.tracef("Negotiated mechanism %s", actualMech);
if (KERBEROS_V5.equals(actualMech) == false) {
throw log.mechNegotiatedMechanismWasNotKerberosV5(getMechanismName()).toSaslException();
}
setNegotiationState(SECURITY_LAYER_ADVERTISER);
if (response == null || response.length == 0) {
log.trace("No response so triggering next state immediately.");
return evaluateMessage(null);
}
} else {
log.trace("GSSContext not established, expecting subsequent exchange.");
}
return response;
} catch (GSSException e) {
throw log.mechUnableToAcceptClientMessage(getMechanismName(), e).toSaslException();
}
case SECURITY_LAYER_ADVERTISER:
// This state expects at most to be called with an empty message, it will then advertise
// the currently support security layer and transition to the next state to await a response
if (message != null && message.length > 0) {
throw log.mechInitialChallengeMustBeEmpty(getMechanismName()).toSaslException();
}
byte[] response = new byte[4];
byte supportedSecurityLayers = 0x00;
boolean offeringSecurityLayer = false;
for (QOP current : orderedQops) {
switch (current) {
case AUTH_INT:
if (gssContext.getIntegState()) {
supportedSecurityLayers |= current.getValue();
offeringSecurityLayer = true;
log.trace("Offering AUTH_INT");
} else {
log.trace("No integrity protection so unable to offer AUTH_INT");
}
break;
case AUTH_CONF:
if (gssContext.getConfState()) {
supportedSecurityLayers |= current.getValue();
offeringSecurityLayer = true;
log.trace("Offering AUTH_CONF");
} else {
log.trace("No confidentiality available so unable to offer AUTH_CONF");
}
break;
default:
supportedSecurityLayers |= current.getValue();
}
}
if (supportedSecurityLayers == 0x00) {
throw log.mechInsufficientQopsAvailable(getMechanismName()).toSaslException();
}
response[0] = supportedSecurityLayers;
try {
byte[] length;
if (offeringSecurityLayer) {
log.tracef("Our max buffer size %d", configuredMaxReceiveBuffer);
length = intToNetworkOrderBytes(configuredMaxReceiveBuffer);
} else {
log.trace("Not offering a security layer so zero length.");
length = new byte[] { 0x00, 0x00, 0x00 };
}
System.arraycopy(length, 0, response, 1, 3);
MessageProp msgProp = new MessageProp(0, false);
response = gssContext.wrap(response, 0, 4, msgProp);
} catch (GSSException e) {
throw log.mechUnableToGenerateChallenge(getMechanismName(), e).toSaslException();
}
log.trace("Transitioning to receive chosen security layer from client");
offeredSecurityLayer = supportedSecurityLayers;
setNegotiationState(SECURITY_LAYER_RECEIVER);
return response;
case SECURITY_LAYER_RECEIVER:
MessageProp msgProp = new MessageProp(0, false);
byte[] unwrapped;
try {
unwrapped = gssContext.unwrap(message, 0, message.length, msgProp);
} catch (GSSException e) {
throw log.mechUnableToUnwrapMessage(getMechanismName(), e).toSaslException();
}
if (unwrapped.length < 4) {
throw log.mechInvalidMessageOnUnwrapping(getMechanismName(), unwrapped.length).toSaslException();
}
// What we offered and our own list of QOP could be different so we compare against what we offered as we know we
// only offered it if the underlying GssContext also supports it.
if ((offeredSecurityLayer & unwrapped[0]) == 0x00) {
throw log.mechSelectedUnofferedQop(getMechanismName()).toSaslException();
}
QOP selectedQop = QOP.mapFromValue(unwrapped[0]);
assert selectedQop != null;
maxBuffer = networkOrderBytesToInt(unwrapped, 1, 3);
log.tracef("Client selected security layer %s, with maxBuffer of %d", selectedQop, maxBuffer);
if (relaxComplianceChecks == false && selectedQop == QOP.AUTH && maxBuffer != 0) {
throw log.mechNoSecurityLayerButLengthReceived(getMechanismName()).toSaslException();
}
try {
maxBuffer = gssContext.getWrapSizeLimit(0, selectedQop == QOP.AUTH_CONF, maxBuffer);
} catch (GSSException e) {
throw log.mechUnableToGetMaximumSizeOfMessage(getMechanismName(), e).toSaslException();
}
this.selectedQop = selectedQop;
final String authenticationId;
try {
authenticationId = gssContext.getSrcName().toString();
} catch (GSSException e) {
throw log.mechUnableToDeterminePeerName(getMechanismName(), e).toSaslException();
}
final String authorizationId;
if (unwrapped.length > 4) {
authorizationId = new String(unwrapped, 4, unwrapped.length - 4, StandardCharsets.UTF_8);
} else {
authorizationId = authenticationId;
}
log.tracef("Authentication ID=%s, Authorization ID=%s", authenticationId, authorizationId);
AuthorizeCallback cb = new AuthorizeCallback(authenticationId, authorizationId);
handleCallbacks(new Callback[] {cb});
if (cb.isAuthorized() == false) {
throw log.mechAuthorizationFailed(getMechanismName(), authenticationId, authorizationId).toSaslException();
}
this.authorizationId = authorizationId;
if (selectedQop != QOP.AUTH) {
log.trace("Setting message wrapper.");
setWrapper(new GssapiWrapper(selectedQop == QOP.AUTH_CONF));
}
log.trace("Negotiation complete.");
negotiationComplete();
// By now this is the end.
return null;
}
throw Assert.impossibleSwitchCase(state);
}
}