blob: e65e26798bdcd2178a2462b46aa33f63473bf30f [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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 com.android.ike.ikev2;
import static com.android.ike.ikev2.IkeSessionStateMachine.CMD_LOCAL_REQUEST_CREATE_CHILD;
import static com.android.ike.ikev2.SaProposal.DH_GROUP_NONE;
import static com.android.ike.ikev2.message.IkeHeader.ExchangeType;
import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE;
import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_SA;
import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_TS_INITIATOR;
import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_TS_RESPONDER;
import android.annotation.Nullable;
import android.content.Context;
import android.net.IpSecManager;
import android.net.IpSecManager.ResourceUnavailableException;
import android.net.IpSecManager.SecurityParameterIndex;
import android.net.IpSecManager.SpiUnavailableException;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.os.Looper;
import android.os.Message;
import android.util.Pair;
import com.android.ike.ikev2.SaRecord.ChildSaRecord;
import com.android.ike.ikev2.crypto.IkeCipher;
import com.android.ike.ikev2.crypto.IkeMacIntegrity;
import com.android.ike.ikev2.crypto.IkeMacPrf;
import com.android.ike.ikev2.exceptions.IkeProtocolException;
import com.android.ike.ikev2.exceptions.NoValidProposalChosenException;
import com.android.ike.ikev2.exceptions.TsUnacceptableException;
import com.android.ike.ikev2.message.IkeHeader;
import com.android.ike.ikev2.message.IkeKePayload;
import com.android.ike.ikev2.message.IkeMessage;
import com.android.ike.ikev2.message.IkeNoncePayload;
import com.android.ike.ikev2.message.IkeNotifyPayload;
import com.android.ike.ikev2.message.IkePayload;
import com.android.ike.ikev2.message.IkeSaPayload;
import com.android.ike.ikev2.message.IkeSaPayload.ChildProposal;
import com.android.ike.ikev2.message.IkeSaPayload.DhGroupTransform;
import com.android.ike.ikev2.message.IkeTsPayload;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import java.io.IOException;
import java.net.InetAddress;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.util.ArrayList;
import java.util.List;
/**
* ChildSessionStateMachine tracks states and manages exchanges of this Child Session.
*
* <p>ChildSessionStateMachine has two types of states. One type are states where there is no
* ongoing procedure affecting Child Session (non-procedure state), including Initial, Closed, Idle
* and Receiving. All other states are "procedure" states which are named as follows:
*
* <pre>
* State Name = [Procedure Type] + [Exchange Initiator] + [Exchange Type].
* - An IKE procedure consists of one or two IKE exchanges:
* Procedure Type = {CreateChild | DeleteChild | Info | RekeyChild | SimulRekeyChild}.
* - Exchange Initiator indicates whether local or remote peer is the exchange initiator:
* Exchange Initiator = {Local | Remote}
* - Exchange type defines the function of this exchange.
* Exchange Type = {Create | Delete}
* </pre>
*/
public class ChildSessionStateMachine extends StateMachine {
private static final String TAG = "ChildSessionStateMachine";
/** Receive request for negotiating first Child SA. */
private static final int CMD_HANDLE_FIRST_CHILD_EXCHANGE = 1;
/** Receive a request from the remote. */
private static final int CMD_HANDLE_RECEIVED_REQUEST = 2;
/** Receive a reponse from the remote. */
private static final int CMD_HANDLE_RECEIVED_RESPONSE = 3;
private final Context mContext;
private final IpSecManager mIpSecManager;
/** User provided configurations. */
private final ChildSessionOptions mChildSessionOptions;
/** Callback to notify IKE Session the state changes. */
private final IChildSessionSmCallback mChildSmCallback;
// TODO: Also store ChildSessionCallback for notifying users.
/** Local address assigned on device. */
private final InetAddress mLocalAddress;
/** Remote address configured by users. */
private final InetAddress mRemoteAddress;
/**
* UDP-Encapsulated socket that allows IPsec traffic to pass through a NAT. Null if UDP
* encapsulation is not needed.
*/
@Nullable private final UdpEncapsulationSocket mUdpEncapSocket;
private final IkeMacPrf mIkePrf;
/** Package private SaProposal that represents the negotiated Child SA proposal. */
@VisibleForTesting SaProposal mSaProposal;
private IkeCipher mChildCipher;
private IkeMacIntegrity mChildIntegrity;
/** SK_d is renewed when IKE SA is rekeyed. */
private byte[] mSkD;
/** Package private */
@VisibleForTesting ChildSaRecord mCurrentChildSaRecord;
private final State mInitial = new Initial();
private final State mCreateChildLocalCreate = new CreateChildLocalCreate();
private final State mClosed = new Closed();
private final State mIdle = new Idle();
/** Package private */
ChildSessionStateMachine(
String name,
Looper looper,
Context context,
IpSecManager ipSecManager,
ChildSessionOptions sessionOptions,
IChildSessionSmCallback childSmCallback,
InetAddress localAddress,
InetAddress remoteAddress,
UdpEncapsulationSocket udpEncapSocket,
IkeMacPrf ikePrf,
byte[] skD) {
super(name, looper);
mContext = context;
mIpSecManager = ipSecManager;
mChildSessionOptions = sessionOptions;
mChildSmCallback = childSmCallback;
mLocalAddress = localAddress;
mRemoteAddress = remoteAddress;
mUdpEncapSocket = udpEncapSocket;
mIkePrf = ikePrf;
mSkD = skD;
addState(mInitial);
addState(mCreateChildLocalCreate);
addState(mClosed);
addState(mIdle);
setInitialState(mInitial);
}
/**
* Interface for ChildSessionStateMachine to notify IkeSessionStateMachine of state changes.
*
* <p>Child Session may encounter an IKE Session fatal error in three cases with different
* handling rules:
*
* <pre>
* - When there is a fatal error in an inbound request, onOutboundPayloadsReady will be
* called first to send out an error notification and then onFatalIkeSessionError(false)
* will be called to locally close the IKE Session.
* - When there is a fatal error in an inbound response, only onFatalIkeSessionError(true)
* will be called to notify the remote with a Delete request and then close the IKE Session.
* - When there is an fatal error notification in an inbound response, only
* onFatalIkeSessionError(false) is called to close the IKE Session locally.
* </pre>
*
* <p>Package private.
*/
interface IChildSessionSmCallback {
/** Notify that new Child SA is created. */
void onChildSaCreated(int remoteSpi, ChildSessionStateMachine childSession);
/** Notify that a Child SA is deleted. */
void onChildSaDeleted(int remoteSpi);
/** Notify the IKE Session to send out IKE message for this Child Session. */
void onOutboundPayloadsReady(
@ExchangeType int exchangeType, boolean isResp, List<IkePayload> payloadList);
/** Notify that a Child procedure has been finished. */
void onProcedureFinished();
/**
* Notify that a Child procedure has been finished and the IKE Session should close itself
* because of a fatal error.
*
* <p>The IKE Session should send a Delete IKE request before closing when needsNotifyRemote
* is true.
*/
void onFatalIkeSessionError(boolean needsNotifyRemote);
}
/**
* Receive requesting and responding payloads for negotiating first Child SA.
*
* <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
* as an asynchronous job to the ChildStateMachine handler.
*
* @param reqPayloads SA negotiation related payloads in IKE_AUTH request.
* @param respPayloads SA negotiation related payloads in IKE_AUTH response.
*/
public void handleFirstChildExchange(
List<IkePayload> reqPayloads, List<IkePayload> respPayloads) {
registerProvisionalChildSession(respPayloads);
sendMessage(
CMD_HANDLE_FIRST_CHILD_EXCHANGE,
new FirstChildNegotiationData(reqPayloads, respPayloads));
}
/**
* Initiate Create Child procedure.
*
* <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
* as an asynchronous job to the ChildStateMachine handler.
*/
public void createChildSa() {
sendMessage(CMD_LOCAL_REQUEST_CREATE_CHILD);
}
// TODO: Add receiveRequest(), rekeyChildSa() and deleteChildSa()
/**
* Receive a response.
*
* <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
* as an asynchronous job to the ChildStateMachine handler.
*
* @param exchangeType the exchange type in the response message that needs validation.
* @param payloadList the Child-procedure-related payload list in the response message that
* needs validation.
*/
public void receiveResponse(@ExchangeType int exchangeType, List<IkePayload> payloadList) {
// If we are waiting for a Create/RekeyCreate response and the received message contains SA
// payload we need to register for this provisional Child.
if (isAwaitingCreateResp()
&& IkePayload.getPayloadForTypeInProvidedList(
PAYLOAD_TYPE_SA, IkeSaPayload.class, payloadList)
!= null) {
registerProvisionalChildSession(payloadList);
}
sendMessage(CMD_HANDLE_RECEIVED_RESPONSE, new ReceivedResponse(exchangeType, payloadList));
}
private boolean isAwaitingCreateResp() {
// TODO: Also check if Child Session is waiting for Rekey Create response.
return getCurrentState() == mCreateChildLocalCreate;
}
/**
* Update SK_d with provided value when IKE SA is rekeyed.
*
* @param skD the new skD in byte array.
*/
public void setSkD(byte[] skD) {
mSkD = skD;
}
/**
* Register provisioning ChildSessionStateMachine in IChildSessionSmCallback
*
* <p>This method is for avoiding CHILD_SA_NOT_FOUND error in IkeSessionStateMachine when remote
* peer sends request for delete/rekey this Child SA before ChildSessionStateMachine sends
* FirstChildNegotiationData to itself.
*/
private void registerProvisionalChildSession(List<IkePayload> respPayloads) {
// When decoding responding IkeSaPayload in IkeSessionStateMachine, it is validated that
// IkeSaPayload has exactly one IkeSaPayload.Proposal.
IkeSaPayload saPayload = null;
for (IkePayload payload : respPayloads) {
if (payload.payloadType == IkePayload.PAYLOAD_TYPE_SA) {
saPayload = (IkeSaPayload) payload;
break;
}
}
if (saPayload == null) {
throw new IllegalArgumentException(
"Receive no SA payload for first Child SA negotiation.");
}
// IkeSaPayload.Proposal stores SPI in long type so as to be applied to both 8-byte IKE SPI
// and 4-byte Child SPI. Here we cast the stored SPI to int to represent a Child SPI.
int remoteGenSpi = (int) (saPayload.proposalList.get(0).spi);
mChildSmCallback.onChildSaCreated(remoteGenSpi, this);
}
/**
* FirstChildNegotiationData contains payloads for negotiating first Child SA in IKE_AUTH
* request and IKE_AUTH response and callback to notify IkeSessionStateMachine the SA
* negotiation result.
*/
private static class FirstChildNegotiationData {
public final List<IkePayload> requestPayloads;
public final List<IkePayload> responsePayloads;
FirstChildNegotiationData(List<IkePayload> reqPayloads, List<IkePayload> respPayloads) {
requestPayloads = reqPayloads;
responsePayloads = respPayloads;
}
}
/**
* ReceivedResponse contains exchange type and payloads that are extracted from a response
* message to the current Child procedure.
*/
private static class ReceivedResponse {
public final int exchangeType;
public final List<IkePayload> responsePayloads;
ReceivedResponse(@ExchangeType int eType, List<IkePayload> respPayloads) {
exchangeType = eType;
responsePayloads = respPayloads;
}
}
/** Initial state of ChildSessionStateMachine. */
class Initial extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
// TODO: Handle local request for creating Child SA.
case CMD_HANDLE_FIRST_CHILD_EXCHANGE:
boolean childSetUpSuccess = false;
Pair<SecurityParameterIndex, SecurityParameterIndex> childSpiPair = null;
FirstChildNegotiationData childNegotiationData =
(FirstChildNegotiationData) message.obj;
try {
List<IkePayload> reqPayloads = childNegotiationData.requestPayloads;
List<IkePayload> respPayloads = childNegotiationData.responsePayloads;
childSpiPair = validateCreateChildResp(reqPayloads, respPayloads);
mCurrentChildSaRecord =
ChildSaRecord.makeChildSaRecord(
mContext,
reqPayloads,
respPayloads,
childSpiPair.first,
childSpiPair.second,
mLocalAddress,
mRemoteAddress,
mUdpEncapSocket,
mIkePrf,
mChildIntegrity,
mChildCipher,
mSkD,
mChildSessionOptions.isTransportMode(),
true /*isLocalInit*/);
// TODO: Add mCurrentChildSaRecord in mSpiToSaRecordMap.
childSetUpSuccess = true;
transitionTo(mIdle);
} catch (IkeProtocolException e) {
// TODO: Unregister remotely generated SPI and handle Child SA negotiation
// failure.
} catch (GeneralSecurityException e) {
// TODO: Handle DH shared key calculation failure.
} catch (ResourceUnavailableException
| SpiUnavailableException
| IOException e) {
// TODO:Fire the ChildSessionCallback.onError() callback and initiate
// deletion exchange on this Child SA.
} finally {
if (!childSetUpSuccess && childSpiPair != null) {
childSpiPair.first.close();
childSpiPair.second.close();
}
}
return HANDLED;
case CMD_LOCAL_REQUEST_CREATE_CHILD:
transitionTo(mCreateChildLocalCreate);
return HANDLED;
default:
return NOT_HANDLED;
}
}
private Pair<SecurityParameterIndex, SecurityParameterIndex> validateCreateChildResp(
List<IkePayload> reqPayloads, List<IkePayload> respPayloads)
throws IkeProtocolException, ResourceUnavailableException, SpiUnavailableException {
// TODO: If the response is unacceptable, extract the corresponding Child SPI in SA
// request and initiate Delete Child SA exchange. If the response includes an error
// notification, clean up this StateMachine.
List<IkeNotifyPayload> notifyPayloads =
IkePayload.getPayloadListForTypeInProvidedList(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class, respPayloads);
boolean hasTransportNotify = false;
for (IkeNotifyPayload notify : notifyPayloads) {
switch (notify.notifyType) {
case IkeNotifyPayload.NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE:
// TODO: Store it as part of negotiation results that can be retrieved by
// users.
break;
case IkeNotifyPayload.NOTIFY_TYPE_IPCOMP_SUPPORTED:
// Ignore
break;
case IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE:
hasTransportNotify = true;
break;
case IkeNotifyPayload.NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED:
// Ignore
break;
default:
throw new UnsupportedOperationException(
"Do not support handling error notifications");
// TODO: Throw IkeProtocolException if encountering error notifications.
}
}
if (mChildSessionOptions.isTransportMode() != hasTransportNotify) {
throw new NoValidProposalChosenException(
"Failed the negotiation on Child SA mode (conflicting modes chosen).");
}
validateTsPayloads(reqPayloads, respPayloads);
IkeSaPayload reqSaPayload =
IkePayload.getPayloadForTypeInProvidedList(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads);
IkeSaPayload respSaPayload =
IkePayload.getPayloadForTypeInProvidedList(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, respPayloads);
// This method either throws exception or returns non-null pair that contains two valid
// {@link ChildProposal} both with a {@link SecurityParameterIndex} allocated inside.
Pair<ChildProposal, ChildProposal> childProposalPair =
respSaPayload.getVerifiedNegotiatedChildProposalPair(
reqSaPayload, mIpSecManager, mRemoteAddress);
mSaProposal = childProposalPair.second.saProposal;
try {
// Build crypto tools using mSaProposal. It is ensured by {@link
// IkeSaPayload#getVerifiedNegotiatedChildProposalPair} that mSaProposal is valid.
// mSaProposal has exactly one encryption algorithm. When the encryption algorithm
// is combined-mode, it either does not have integrity algorithm or only has one
// NONE value integrity algorithm. Otherwise, it has at most one integrity
// algorithm.
Provider provider = IkeMessage.getSecurityProvider();
mChildCipher = IkeCipher.create(mSaProposal.getEncryptionTransforms()[0], provider);
if (mSaProposal.getIntegrityTransforms().length != 0
&& mSaProposal.getIntegrityTransforms()[0].id
!= SaProposal.INTEGRITY_ALGORITHM_NONE) {
mChildIntegrity =
IkeMacIntegrity.create(
mSaProposal.getIntegrityTransforms()[0], provider);
}
return new Pair<SecurityParameterIndex, SecurityParameterIndex>(
childProposalPair.first.getChildSpiResource(),
childProposalPair.second.getChildSpiResource());
} catch (Exception e) {
childProposalPair.first.getChildSpiResource().close();
childProposalPair.second.getChildSpiResource().close();
throw e;
}
}
private void validateTsPayloads(List<IkePayload> reqPayloads, List<IkePayload> respPayloads)
throws TsUnacceptableException {
for (int tsType : new int[] {PAYLOAD_TYPE_TS_INITIATOR, PAYLOAD_TYPE_TS_RESPONDER}) {
IkeTsPayload reqPayload =
IkePayload.getPayloadForTypeInProvidedList(
tsType, IkeTsPayload.class, reqPayloads);
IkeTsPayload respPayload =
IkePayload.getPayloadForTypeInProvidedList(
tsType, IkeTsPayload.class, respPayloads);
if (!reqPayload.contains(respPayload)) {
throw new TsUnacceptableException();
}
}
}
}
/**
* CreateChildLocalCreate represents the state where Child Session initiates the Create Child
* exchange.
*/
class CreateChildLocalCreate extends State {
private List<IkePayload> mRequestPayloads;
@Override
public void enter() {
try {
mRequestPayloads =
CreateChildSaHelper.getInitCreateSaRequestPayloads(
mIpSecManager, mLocalAddress, mChildSessionOptions);
mChildSmCallback.onOutboundPayloadsReady(
IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
false /*isResp*/,
mRequestPayloads);
} catch (ResourceUnavailableException e) {
// TODO: Notify users and close the Child Session.
}
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_HANDLE_RECEIVED_RESPONSE:
// TODO: Validate the response against the cached request and construct
// ChildSaRecord.
transitionTo(mIdle);
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
/**
* Closed represents the state when this ChildSessionStateMachine is closed, and no further
* actions can be performed on it.
*/
class Closed extends State {
// TODO: Implement it.
}
/**
* Idle represents a state when there is no ongoing IKE exchange affecting established Child SA.
*/
class Idle extends State {
@Override
public void enter() {
mChildSmCallback.onProcedureFinished();
}
// TODO: Support handling local and remote request.
}
/**
* Package private helper class to generate IKE SA creation payloads, in both request and
* response directions.
*/
static class CreateChildSaHelper {
/** Create payload list for creating the initial Child SA for this Child Session. */
public static List<IkePayload> getInitCreateSaRequestPayloads(
IpSecManager ipSecManager,
InetAddress localAddress,
ChildSessionOptions childSessionOptions)
throws ResourceUnavailableException {
// TODO: b/134625950 Do not include DH Transform and KE Payload when this method is
// called for creating the first Child SA under the IKE Session.
return getCreateSaPayloads(
false /*isResp*/,
childSessionOptions.isTransportMode(),
ipSecManager,
localAddress,
childSessionOptions.getSaProposals(),
childSessionOptions.getLocalTrafficSelectors(),
childSessionOptions.getRemoteTrafficSelectors());
}
// TODO: Support creating payloads for rekey request and response by calling
// #getCreateSaPayloads and adding Notify-Rekey payload.
/** Create payload list for creating a new Child SA. */
private static List<IkePayload> getCreateSaPayloads(
boolean isResp,
boolean isTransport,
IpSecManager ipSecManager,
InetAddress localAddress,
SaProposal[] saProposals,
IkeTrafficSelector[] initTs,
IkeTrafficSelector[] respTs)
throws ResourceUnavailableException {
List<IkePayload> payloadList = new ArrayList<>(5);
payloadList.add(
IkeSaPayload.createChildSaPayload(
isResp, saProposals, ipSecManager, localAddress));
payloadList.add(new IkeTsPayload(true /*isInitiator*/, initTs));
payloadList.add(new IkeTsPayload(false /*isInitiator*/, respTs));
payloadList.add(new IkeNoncePayload());
DhGroupTransform[] dhGroups = saProposals[0].getDhGroupTransforms();
if (dhGroups.length != 0 && dhGroups[0].id != DH_GROUP_NONE) {
payloadList.add(new IkeKePayload(dhGroups[0].id));
}
if (isTransport) payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_USE_TRANSPORT_MODE));
return payloadList;
}
}
/** Called when this StateMachine quits. */
@Override
protected void onQuitting() {
mChildSmCallback.onProcedureFinished();
}
// TODO: Add states to support creating additional Child SA, deleting Child SA and rekeying
// Child SA.
}