blob: 5f2e6f7603cad04fbb6aca3957c699cec5254751 [file] [log] [blame]
/*
*
* Copyright (c) 2020-2021 Project CHIP Authors
* All rights reserved.
*
* 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.
*/
/**
* @file
* This file defines a secure transport layer which adds encryption to data
* sent over a transport.
*
*/
#pragma once
#include <utility>
#include <credentials/FabricTable.h>
#include <crypto/RandUtils.h>
#include <crypto/SessionKeystore.h>
#include <inet/IPAddress.h>
#include <lib/core/CHIPCore.h>
#include <lib/core/CHIPPersistentStorageDelegate.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/DLLUtil.h>
#include <messaging/ReliableMessageProtocolConfig.h>
#include <protocols/secure_channel/Constants.h>
#include <transport/CryptoContext.h>
#include <transport/GroupPeerMessageCounter.h>
#include <transport/GroupSession.h>
#include <transport/MessageCounterManagerInterface.h>
#include <transport/SecureSessionTable.h>
#include <transport/Session.h>
#include <transport/SessionDelegate.h>
#include <transport/SessionHolder.h>
#include <transport/SessionMessageDelegate.h>
#include <transport/TransportMgr.h>
#include <transport/UnauthenticatedSessionTable.h>
#include <transport/raw/Base.h>
#include <transport/raw/PeerAddress.h>
#include <transport/raw/Tuple.h>
namespace chip {
/**
* @brief
* Tracks ownership of a encrypted packet buffer.
*
* EncryptedPacketBufferHandle is a kind of PacketBufferHandle class and used to hold a packet buffer
* object whose payload has already been encrypted.
*/
class EncryptedPacketBufferHandle final : private System::PacketBufferHandle
{
public:
EncryptedPacketBufferHandle() {}
EncryptedPacketBufferHandle(EncryptedPacketBufferHandle && aBuffer) : PacketBufferHandle(std::move(aBuffer)) {}
void operator=(EncryptedPacketBufferHandle && aBuffer) { PacketBufferHandle::operator=(std::move(aBuffer)); }
using System::PacketBufferHandle::IsNull;
// Pass-through to HasChainedBuffer on our underlying buffer without
// exposing operator->
bool HasChainedBuffer() const { return (*this)->HasChainedBuffer(); }
uint32_t GetMessageCounter() const;
/**
* Creates a copy of the data in this packet.
*
* Does NOT support chained buffers.
*
* @returns empty handle on allocation failure.
*/
EncryptedPacketBufferHandle CloneData() { return EncryptedPacketBufferHandle(PacketBufferHandle::CloneData()); }
#ifdef CHIP_ENABLE_TEST_ENCRYPTED_BUFFER_API
/**
* Extracts the (unencrypted) packet header from this encrypted packet
* buffer. Returns error if a packet header cannot be extracted (e.g. if
* there are not enough bytes in this packet buffer). After this call the
* buffer does not have a packet header. This API is meant for
* unit tests only. The CHIP_ENABLE_TEST_ENCRYPTED_BUFFER_API define
* should not be defined normally.
*/
CHIP_ERROR ExtractPacketHeader(PacketHeader & aPacketHeader) { return aPacketHeader.DecodeAndConsume(*this); }
/**
* Inserts a new (unencrypted) packet header in the encrypted packet buffer
* based on the given PacketHeader. This API is meant for
* unit tests only. The CHIP_ENABLE_TEST_ENCRYPTED_BUFFER_API define
* should not be defined normally.
*/
CHIP_ERROR InsertPacketHeader(const PacketHeader & aPacketHeader) { return aPacketHeader.EncodeBeforeData(*this); }
#endif // CHIP_ENABLE_TEST_ENCRYPTED_BUFFER_API
static EncryptedPacketBufferHandle MarkEncrypted(PacketBufferHandle && aBuffer)
{
return EncryptedPacketBufferHandle(std::move(aBuffer));
}
/**
* Get a handle to the data that allows mutating the bytes. This should
* only be used if absolutely necessary, because EncryptedPacketBufferHandle
* represents a buffer that we want to resend as-is.
*/
PacketBufferHandle CastToWritable() const { return PacketBufferHandle::Retain(); }
private:
EncryptedPacketBufferHandle(PacketBufferHandle && aBuffer) : PacketBufferHandle(std::move(aBuffer)) {}
};
class DLL_EXPORT SessionManager : public TransportMgrDelegate, public FabricTable::Delegate
{
public:
SessionManager();
~SessionManager() override;
/**
* @brief
* This function takes the payload and returns an encrypted message which can be sent multiple times.
*
* @details
* It does the following:
* 1. Encrypt the msgBuf
* 2. construct the packet header
* 3. Encode the packet header and prepend it to message.
* Returns a encrypted message in encryptedMessage.
*/
CHIP_ERROR PrepareMessage(const SessionHandle & session, PayloadHeader & payloadHeader, System::PacketBufferHandle && msgBuf,
EncryptedPacketBufferHandle & encryptedMessage);
/**
* @brief
* Send a prepared message to a currently connected peer.
*/
CHIP_ERROR SendPreparedMessage(const SessionHandle & session, const EncryptedPacketBufferHandle & preparedMessage);
/// @brief Set the delegate for handling incoming messages. There can be only one message delegate (probably the
/// ExchangeManager)
void SetMessageDelegate(SessionMessageDelegate * cb) { mCB = cb; }
// Test-only: create a session on the fly.
CHIP_ERROR InjectPaseSessionWithTestKey(SessionHolder & sessionHolder, uint16_t localSessionId, NodeId peerNodeId,
uint16_t peerSessionId, FabricIndex fabricIndex,
const Transport::PeerAddress & peerAddress, CryptoContext::SessionRole role);
CHIP_ERROR InjectCaseSessionWithTestKey(SessionHolder & sessionHolder, uint16_t localSessionId, uint16_t peerSessionId,
NodeId localNodeId, NodeId peerNodeId, FabricIndex fabric,
const Transport::PeerAddress & peerAddress, CryptoContext::SessionRole role,
const CATValues & cats = CATValues{});
/**
* @brief
* Allocate a secure session and non-colliding session ID in the secure
* session table.
*
* If we're either establishing or just finished establishing a session to a peer in either initiator or responder
* roles, the node id of that peer should be provided in sessionEvictionHint. Else, it should be initialized
* to a default-constructed ScopedNodeId().
*
* @return SessionHandle with a reference to a SecureSession, else NullOptional on failure
*/
CHECK_RETURN_VALUE
Optional<SessionHandle> AllocateSession(Transport::SecureSession::Type secureSessionType,
const ScopedNodeId & sessionEvictionHint);
/**
* A set of templated helper function that call a provided lambda
* on all sessions in the underlying session table that match the provided
* query criteria.
*
*/
/**
* Call the provided lambda on sessions whose remote side match the provided ScopedNodeId.
*
*/
template <typename Function>
void ForEachMatchingSession(const ScopedNodeId & node, Function && function)
{
mSecureSessions.ForEachSession([&](auto * session) {
if (session->GetPeer() == node)
{
function(session);
}
return Loop::Continue;
});
}
/**
* Call the provided lambda on sessions that match the provided fabric index.
*
*/
template <typename Function>
void ForEachMatchingSession(FabricIndex fabricIndex, Function && function)
{
mSecureSessions.ForEachSession([&](auto * session) {
if (session->GetFabricIndex() == fabricIndex)
{
function(session);
}
return Loop::Continue;
});
}
/**
* Call the provided lambda on all sessions whose remote side match the logical fabric
* associated with the provided ScopedNodeId and target the same logical remote node.
*
* *NOTE* This is identical in behavior to ForEachMatchingSession(const ScopedNodeId ..)
* EXCEPT if there are multiple FabricInfo instances in the FabricTable that collide
* on the same logical fabric (i.e root public key + fabric ID tuple).
* This can ONLY happen if multiple controller instances on the same fabric is permitted
* and each is assigned a unique fabric index.
*/
template <typename Function>
CHIP_ERROR ForEachMatchingSessionOnLogicalFabric(const ScopedNodeId & node, Function && function)
{
Crypto::P256PublicKey targetPubKey;
auto * targetFabric = mFabricTable->FindFabricWithIndex(node.GetFabricIndex());
VerifyOrReturnError(targetFabric != nullptr, CHIP_ERROR_INVALID_FABRIC_INDEX);
auto err = targetFabric->FetchRootPubkey(targetPubKey);
VerifyOrDie(err == CHIP_NO_ERROR);
mSecureSessions.ForEachSession([&](auto * session) {
Crypto::P256PublicKey comparePubKey;
//
// It's entirely possible to either come across a PASE session OR, a CASE session
// that has yet to be activated (i.e a CASEServer holding onto a SecureSession object
// waiting for a Sigma1 message to arrive). Let's skip those.
//
if (!session->IsCASESession() || session->GetFabricIndex() == kUndefinedFabricIndex)
{
return Loop::Continue;
}
auto * compareFabric = mFabricTable->FindFabricWithIndex(session->GetFabricIndex());
VerifyOrDie(compareFabric != nullptr);
err = compareFabric->FetchRootPubkey(comparePubKey);
VerifyOrDie(err == CHIP_NO_ERROR);
if (comparePubKey.Matches(targetPubKey) && targetFabric->GetFabricId() == compareFabric->GetFabricId() &&
session->GetPeerNodeId() == node.GetNodeId())
{
function(session);
}
return Loop::Continue;
});
return CHIP_NO_ERROR;
}
/**
* Call the provided lambda on all sessions that match the logical fabric
* associated with the provided fabric index.
*
* *NOTE* This is identical in behavior to ForEachMatchingSession(FabricIndex ..)
* EXCEPT if there are multiple FabricInfo instances in the FabricTable that collide
* on the same logical fabric (i.e root public key + fabric ID tuple).
* This can ONLY happen if multiple controller instances on the same fabric is permitted
* and each is assigned a unique fabric index.
*/
template <typename Function>
CHIP_ERROR ForEachMatchingSessionOnLogicalFabric(FabricIndex fabricIndex, Function && function)
{
Crypto::P256PublicKey targetPubKey;
auto * targetFabric = mFabricTable->FindFabricWithIndex(fabricIndex);
VerifyOrReturnError(targetFabric != nullptr, CHIP_ERROR_INVALID_FABRIC_INDEX);
auto err = targetFabric->FetchRootPubkey(targetPubKey);
VerifyOrDie(err == CHIP_NO_ERROR);
mSecureSessions.ForEachSession([&](auto * session) {
Crypto::P256PublicKey comparePubKey;
//
// It's entirely possible to either come across a PASE session OR, a CASE session
// that has yet to be activated (i.e a CASEServer holding onto a SecureSession object
// waiting for a Sigma1 message to arrive). Let's skip those.
//
if (!session->IsCASESession() || session->GetFabricIndex() == kUndefinedFabricIndex)
{
return Loop::Continue;
}
auto * compareFabric = mFabricTable->FindFabricWithIndex(session->GetFabricIndex());
VerifyOrDie(compareFabric != nullptr);
err = compareFabric->FetchRootPubkey(comparePubKey);
VerifyOrDie(err == CHIP_NO_ERROR);
if (comparePubKey.Matches(targetPubKey) && targetFabric->GetFabricId() == compareFabric->GetFabricId())
{
function(session);
}
return Loop::Continue;
});
return CHIP_NO_ERROR;
}
void ExpireAllSessions(const ScopedNodeId & node);
void ExpireAllSessionsForFabric(FabricIndex fabricIndex);
/**
* Expire all sessions whose remote side matches the logical fabric
* associated with the provided ScopedNodeId and target the same logical remote node.
*
* *NOTE* This is identical in behavior to ExpireAllSessions(const ScopedNodeId ..)
* EXCEPT if there are multiple FabricInfo instances in the FabricTable that collide
* on the same logical fabric (i.e root public key + fabric ID tuple). This can ONLY happen
* if multiple controller instances on the same fabric is permitted and each is assigned
* a unique fabric index.
*
*/
CHIP_ERROR ExpireAllSessionsOnLogicalFabric(const ScopedNodeId & node);
/**
* Expire all sessions whose remote side matches the logical fabric
* associated with the provided fabric index.
*
* *NOTE* This is identical in behavior to ExpireAllSessExpireAllSessionsForFabricions(FabricIndex ..)
* EXCEPT if there are multiple FabricInfo instances in the FabricTable that collide
* on the same logical fabric (i.e root public key + fabric ID tuple). This can ONLY happen
* if multiple controller instances on the same fabric is permitted and each is assigned
* a unique fabric index.
*
*/
CHIP_ERROR ExpireAllSessionsOnLogicalFabric(FabricIndex fabricIndex);
void ExpireAllPASESessions();
/**
* @brief
* Marks all active sessions that match provided arguments as defunct.
*
* @param node Scoped node ID of the active sessions we should mark as defunct.
* @param type Type of session we are looking to mark as defunct. If matching
* against all types of sessions is desired, NullOptional should
* be passed into type.
*/
void MarkSessionsAsDefunct(const ScopedNodeId & node, const Optional<Transport::SecureSession::Type> & type);
/**
* @brief
* Update all CASE sessions that match `node` with the provided transport peer address.
*
* @param node Scoped node ID of the active sessions we want to update.
* @param addr Transport peer address that we want to update to.
*/
void UpdateAllSessionsPeerAddress(const ScopedNodeId & node, const Transport::PeerAddress & addr);
/**
* @brief
* Return the System Layer pointer used by current SessionManager.
*/
System::Layer * SystemLayer() { return mSystemLayer; }
/**
* @brief
* Initialize a Secure Session Manager
*
* @param systemLayer System layer to use
* @param transportMgr Transport to use
* @param messageCounterManager The message counter manager
* @param storageDelegate Persistent storage implementation
* @param fabricTable Fabric table to hold information about joined fabrics
* @param sessionKeystore Session keystore for management of symmetric encryption keys
*/
CHIP_ERROR Init(System::Layer * systemLayer, TransportMgrBase * transportMgr,
Transport::MessageCounterManagerInterface * messageCounterManager,
chip::PersistentStorageDelegate * storageDelegate, FabricTable * fabricTable,
Crypto::SessionKeystore & sessionKeystore);
/**
* @brief
* Shutdown the Secure Session Manager. This terminates this instance
* of the object and reset it's state.
*/
void Shutdown();
/**
* @brief Notification that a fabric was removed.
*/
void FabricRemoved(FabricIndex fabricIndex);
TransportMgrBase * GetTransportManager() const { return mTransportMgr; }
Transport::SecureSessionTable & GetSecureSessions() { return mSecureSessions; }
/**
* @brief
* Handle received secure message. Implements TransportMgrDelegate
*
* @param source the source address of the package
* @param msgBuf the buffer containing a full CHIP message (except for the optional length field).
*/
void OnMessageReceived(const Transport::PeerAddress & source, System::PacketBufferHandle && msgBuf) override;
Optional<SessionHandle> CreateUnauthenticatedSession(const Transport::PeerAddress & peerAddress,
const ReliableMessageProtocolConfig & config)
{
// Allocate ephemeralInitiatorNodeID in Operational Node ID range
NodeId ephemeralInitiatorNodeID;
do
{
ephemeralInitiatorNodeID = static_cast<NodeId>(Crypto::GetRandU64());
} while (!IsOperationalNodeId(ephemeralInitiatorNodeID));
return mUnauthenticatedSessions.AllocInitiator(ephemeralInitiatorNodeID, peerAddress, config);
}
//
// Find an existing secure session given a peer's scoped NodeId and a type of session to match against.
// If matching against all types of sessions is desired, NullOptional should be passed into type.
//
// If a valid session is found, an Optional<SessionHandle> with the value set to the SessionHandle of the session
// is returned. Otherwise, an Optional<SessionHandle> with no value set is returned.
//
//
Optional<SessionHandle> FindSecureSessionForNode(ScopedNodeId peerNodeId,
const Optional<Transport::SecureSession::Type> & type = NullOptional);
using SessionHandleCallback = bool (*)(void * context, SessionHandle & sessionHandle);
CHIP_ERROR ForEachSessionHandle(void * context, SessionHandleCallback callback);
//// FabricTable::Delegate Implementation ////
void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) override
{
(void) fabricTable;
this->FabricRemoved(fabricIndex);
}
FabricTable * GetFabricTable() const { return mFabricTable; }
Crypto::SessionKeystore * GetSessionKeystore() const { return mSessionKeystore; }
private:
/**
* The State of a secure transport object.
*/
enum class State
{
kNotReady, /**< State before initialization. */
kInitialized, /**< State when the object is ready connect to other peers. */
};
enum class EncryptionState
{
kPayloadIsEncrypted,
kPayloadIsUnencrypted,
};
System::Layer * mSystemLayer = nullptr;
FabricTable * mFabricTable = nullptr;
Crypto::SessionKeystore * mSessionKeystore = nullptr;
Transport::UnauthenticatedSessionTable<CHIP_CONFIG_UNAUTHENTICATED_CONNECTION_POOL_SIZE> mUnauthenticatedSessions;
Transport::SecureSessionTable mSecureSessions;
State mState; // < Initialization state of the object
chip::Transport::GroupOutgoingCounters mGroupClientCounter;
SessionMessageDelegate * mCB = nullptr;
TransportMgrBase * mTransportMgr = nullptr;
Transport::MessageCounterManagerInterface * mMessageCounterManager = nullptr;
GlobalUnencryptedMessageCounter mGlobalUnencryptedMessageCounter;
/**
* @brief Parse, decrypt, validate, and dispatch a secure unicast message.
*
* @param[in] partialPacketHeader The partial PacketHeader of the message after processing with DecodeFixed.
* If the message decrypts successfully, this will be filled with a fully decoded PacketHeader.
* @param[in] peerAddress The PeerAddress of the message as provided by the receiving Transport Endpoint.
* @param msg The full message buffer, including header fields.
*/
void SecureUnicastMessageDispatch(const PacketHeader & partialPacketHeader, const Transport::PeerAddress & peerAddress,
System::PacketBufferHandle && msg);
/**
* @brief Parse, decrypt, validate, and dispatch a secure group message.
*
* @param partialPacketHeader The partial PacketHeader of the message once processed with DecodeFixed.
* @param peerAddress The PeerAddress of the message as provided by the receiving Transport Endpoint.
* @param msg The full message buffer, including header fields.
*/
void SecureGroupMessageDispatch(const PacketHeader & partialPacketHeader, const Transport::PeerAddress & peerAddress,
System::PacketBufferHandle && msg);
/**
* @brief Parse, decrypt, validate, and dispatch an unsecured message.
*
* @param partialPacketHeader The partial PacketHeader of the message after processing with DecodeFixed.
* @param peerAddress The PeerAddress of the message as provided by the receiving Transport Endpoint.
* @param msg The full message buffer, including header fields.
*/
void UnauthenticatedMessageDispatch(const PacketHeader & partialPacketHeader, const Transport::PeerAddress & peerAddress,
System::PacketBufferHandle && msg);
void OnReceiveError(CHIP_ERROR error, const Transport::PeerAddress & source);
static bool IsControlMessage(PayloadHeader & payloadHeader)
{
return payloadHeader.HasMessageType(Protocols::SecureChannel::MsgType::MsgCounterSyncReq) ||
payloadHeader.HasMessageType(Protocols::SecureChannel::MsgType::MsgCounterSyncRsp);
}
};
namespace MessagePacketBuffer {
/**
* Maximum size of a message footer, in bytes.
*/
inline constexpr uint16_t kMaxFooterSize = kMaxTagLen;
/**
* Allocates a packet buffer with space for message headers and footers.
*
* Fails and returns \c nullptr if no memory is available, or if the size requested is too large.
*
* @param[in] aAvailableSize Minimum number of octets to for application data.
*
* @return On success, a PacketBufferHandle to the allocated buffer. On fail, \c nullptr.
*/
inline System::PacketBufferHandle New(size_t aAvailableSize)
{
static_assert(System::PacketBuffer::kMaxSize > kMaxFooterSize, "inadequate capacity");
if (aAvailableSize > System::PacketBuffer::kMaxSize - kMaxFooterSize)
{
return System::PacketBufferHandle();
}
return System::PacketBufferHandle::New(aAvailableSize + kMaxFooterSize);
}
/**
* Allocates a packet buffer with initial contents.
*
* @param[in] aData Initial buffer contents.
* @param[in] aDataSize Size of initial buffer contents.
*
* @return On success, a PacketBufferHandle to the allocated buffer. On fail, \c nullptr.
*/
inline System::PacketBufferHandle NewWithData(const void * aData, size_t aDataSize)
{
return System::PacketBufferHandle::NewWithData(aData, aDataSize, kMaxFooterSize);
}
/**
* Check whether a packet buffer has enough space for a message footer.
*
* @returns true if there is space, false otherwise.
*/
inline bool HasFooterSpace(const System::PacketBufferHandle & aBuffer)
{
return aBuffer->AvailableDataLength() >= kMaxFooterSize;
}
} // namespace MessagePacketBuffer
} // namespace chip