Skip to content

Commit

Permalink
[ota] Parse OTA image header in nRF Connect and Linux platforms (#14875)
Browse files Browse the repository at this point in the history
* [ota] Parse OTA image header in nRF Connect and Linux platforms

1. Make the OTA image header parser stateful to ease
   processing small blocks of the image.
2. Use the parser in the image processor for nRF Connect and
   Linux platforms.

* Doc improvement
  • Loading branch information
Damian-Nordic authored Feb 9, 2022
1 parent ce5f36b commit 8ce90ac
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 62 deletions.
1 change: 1 addition & 0 deletions config/nrfconnect/chip-module/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ config CHIP_OTA_REQUESTOR_BUFFER_SIZE
# See config/zephyr/Kconfig for full definition
config CHIP_OTA_IMAGE_BUILD
bool
default y if CHIP_OTA_REQUESTOR
depends on SIGN_IMAGES
2 changes: 2 additions & 0 deletions examples/lighting-app/nrfconnect/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ CONFIG_MPU_STACK_GUARD=y

# CHIP configuration
CONFIG_CHIP_PROJECT_CONFIG="main/include/CHIPProjectConfig.h"
# 20044 == 0x4E4C - "[N]ordic [L]ight"
CONFIG_CHIP_DEVICE_PRODUCT_ID=20044

# Enable CHIP pairing automatically on application start.
CONFIG_CHIP_ENABLE_PAIRING_AUTOSTART=y
4 changes: 1 addition & 3 deletions src/app/clusters/ota-requestor/BDXDownloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ CHIP_ERROR BDXDownloader::HandleBdxEvent(const chip::bdx::TransferSession::Outpu
case TransferSession::OutputEventType::kBlockReceived: {
chip::ByteSpan blockData(outEvent.blockdata.Data, outEvent.blockdata.Length);
ReturnErrorOnFailure(mImageProcessor->ProcessBlock(blockData));
Nullable<uint8_t> percent;
mImageProcessor->GetPercentComplete(percent);
mStateDelegate->OnUpdateProgressChanged(percent);
mStateDelegate->OnUpdateProgressChanged(mImageProcessor->GetPercentComplete());

// TODO: this will cause problems if Finalize() is not guaranteed to do its work after ProcessBlock().
if (outEvent.blockdata.IsEof)
Expand Down
3 changes: 1 addition & 2 deletions src/app/clusters/ota-requestor/OTARequestor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,7 @@ void OTARequestor::RecordErrorUpdateState(UpdateFailureState failureState, CHIP_
// Log the DownloadError event
OTAImageProcessorInterface * imageProcessor = mBdxDownloader->GetImageProcessorDelegate();
VerifyOrReturn(imageProcessor != nullptr);
Nullable<uint8_t> progressPercent;
imageProcessor->GetPercentComplete(progressPercent);
Nullable<uint8_t> progressPercent = imageProcessor->GetPercentComplete();
Nullable<int64_t> platformCode;
OtaRequestorServerOnDownloadError(mTargetVersion, imageProcessor->GetBytesDownloaded(), progressPercent, platformCode);

Expand Down
29 changes: 4 additions & 25 deletions src/include/platform/OTAImageProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,6 @@ struct OTAImageProcessorParams
uint64_t totalFileBytes = 0;
};

// TODO: Parse the header when the image is received
struct OTAImageProcessorHeader
{
uint16_t vendorId;
uint16_t productId;
uint32_t softwareVersion;
CharSpan softwareVersionString;
uint64_t payloadSize;
uint16_t minApplicableSoftwareVersion;
uint16_t maxApplicableSoftwareVersion;
CharSpan releaseNotesUrl;
uint8_t imageDigestType;
ByteSpan imageDigest;
};

/**
* @class OTAImageProcessorInterface
*
Expand Down Expand Up @@ -99,16 +84,11 @@ class DLL_EXPORT OTAImageProcessorInterface
/**
* Called to check the current download status of the OTA image download.
*/
virtual void GetPercentComplete(app::DataModel::Nullable<uint8_t> & percent)
virtual app::DataModel::Nullable<uint8_t> GetPercentComplete()
{
if (mParams.totalFileBytes == 0)
{
percent.SetNull();
}
else
{
percent.SetNonNull(static_cast<uint8_t>((mParams.downloadedBytes * 100) / mParams.totalFileBytes));
}
return mParams.totalFileBytes > 0
? app::DataModel::Nullable<uint8_t>(static_cast<uint8_t>((mParams.downloadedBytes * 100) / mParams.totalFileBytes))
: app::DataModel::Nullable<uint8_t>{};
}

/**
Expand All @@ -118,7 +98,6 @@ class DLL_EXPORT OTAImageProcessorInterface

protected:
OTAImageProcessorParams mParams;
OTAImageProcessorHeader mHeader;
};

} // namespace chip
90 changes: 81 additions & 9 deletions src/lib/core/OTAImageHeader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,93 @@ enum class Tag : uint8_t
kImageDigest = 9,
};

/// Length of the fixed portion of the Matter OTA image header: FileIdentifier (4B), TotalSize (8B) and HeaderSize (4B)
constexpr uint32_t kFixedHeaderSize = 16;

/// Maximum supported Matter OTA image header size
constexpr uint32_t kMaxHeaderSize = 1024;

/// Maximum size of the software version string
constexpr size_t kMaxSoftwareVersionStringSize = 64;

/// Maximum size of the release notes URL
constexpr size_t kMaxReleaseNotesURLSize = 256;

} // namespace

CHIP_ERROR DecodeOTAImageHeader(ByteSpan buffer, OTAImageHeader & header)
void OTAImageHeaderParser::Init()
{
mState = State::kInitialized;
mBufferOffset = 0;
mHeaderTlvSize = 0;
mBuffer.Alloc(kFixedHeaderSize);
}

void OTAImageHeaderParser::Clear()
{
mState = State::kNotInitialized;
mBufferOffset = 0;
mHeaderTlvSize = 0;
mBuffer.Free();
}

CHIP_ERROR OTAImageHeaderParser::AccumulateAndDecode(ByteSpan & buffer, OTAImageHeader & header)
{
Encoding::LittleEndian::Reader reader(buffer);
CHIP_ERROR error = CHIP_NO_ERROR;

if (mState == State::kInitialized)
{
Append(buffer, kFixedHeaderSize - mBufferOffset);
error = DecodeFixed();
}

if (mState == State::kTlv)
{
Append(buffer, mHeaderTlvSize - mBufferOffset);
error = DecodeTlv(header);
}

// Parse the fixed part of the header
if (error != CHIP_NO_ERROR && error != CHIP_ERROR_BUFFER_TOO_SMALL)
{
Clear();
}

return error;
}

void OTAImageHeaderParser::Append(ByteSpan & buffer, uint32_t numBytes)
{
numBytes = chip::min(numBytes, static_cast<uint32_t>(buffer.size()));
memcpy(&mBuffer[mBufferOffset], buffer.data(), numBytes);
mBufferOffset += numBytes;
buffer = buffer.SubSpan(numBytes);
}

CHIP_ERROR OTAImageHeaderParser::DecodeFixed()
{
ReturnErrorCodeIf(mBufferOffset < kFixedHeaderSize, CHIP_ERROR_BUFFER_TOO_SMALL);

Encoding::LittleEndian::Reader reader(mBuffer.Get(), mBufferOffset);
uint32_t fileIdentifier;
ReturnErrorOnFailure(reader.Read32(&fileIdentifier).StatusCode());
VerifyOrReturnError(fileIdentifier == kOTAImageFileIdentifier, CHIP_ERROR_INVALID_FILE_IDENTIFIER);
ReturnErrorOnFailure(reader.Read64(&header.mTotalSize).Read32(&header.mHeaderSize).StatusCode());
uint64_t totalSize;
ReturnErrorOnFailure(reader.Read32(&fileIdentifier).Read64(&totalSize).Read32(&mHeaderTlvSize).StatusCode());
ReturnErrorCodeIf(fileIdentifier != kOTAImageFileIdentifier, CHIP_ERROR_INVALID_FILE_IDENTIFIER);
// Safety check against malicious headers.
ReturnErrorCodeIf(mHeaderTlvSize > kMaxHeaderSize, CHIP_ERROR_NO_MEMORY);
ReturnErrorCodeIf(!mBuffer.Alloc(mHeaderTlvSize), CHIP_ERROR_NO_MEMORY);

// Parse the TLV elements of the header
VerifyOrReturnError(header.mHeaderSize <= reader.Remaining(), CHIP_ERROR_BUFFER_TOO_SMALL);
mState = State::kTlv;
mBufferOffset = 0;

return CHIP_NO_ERROR;
}

CHIP_ERROR OTAImageHeaderParser::DecodeTlv(OTAImageHeader & header)
{
ReturnErrorCodeIf(mBufferOffset < mHeaderTlvSize, CHIP_ERROR_BUFFER_TOO_SMALL);

TLV::TLVReader tlvReader;
tlvReader.Init(buffer.data() + reader.OctetsRead(), header.mHeaderSize);
tlvReader.Init(mBuffer.Get(), mBufferOffset);
ReturnErrorOnFailure(tlvReader.Next(TLV::TLVType::kTLVType_Structure, TLV::AnonymousTag()));

TLV::TLVType outerType;
Expand All @@ -69,6 +139,7 @@ CHIP_ERROR DecodeOTAImageHeader(ByteSpan buffer, OTAImageHeader & header)
ReturnErrorOnFailure(tlvReader.Get(header.mSoftwareVersion));
ReturnErrorOnFailure(tlvReader.Next(TLV::ContextTag(to_underlying(Tag::kSoftwareVersionString))));
ReturnErrorOnFailure(tlvReader.Get(header.mSoftwareVersionString));
ReturnErrorCodeIf(header.mSoftwareVersionString.size() > kMaxSoftwareVersionStringSize, CHIP_ERROR_INVALID_STRING_LENGTH);
ReturnErrorOnFailure(tlvReader.Next(TLV::ContextTag(to_underlying(Tag::kPayloadSize))));
ReturnErrorOnFailure(tlvReader.Get(header.mPayloadSize));
ReturnErrorOnFailure(tlvReader.Next());
Expand All @@ -88,6 +159,7 @@ CHIP_ERROR DecodeOTAImageHeader(ByteSpan buffer, OTAImageHeader & header)
if (tlvReader.GetTag() == TLV::ContextTag(to_underlying(Tag::kReleaseNotesURL)))
{
ReturnErrorOnFailure(tlvReader.Get(header.mReleaseNotesURL));
ReturnErrorCodeIf(header.mReleaseNotesURL.size() > kMaxReleaseNotesURLSize, CHIP_ERROR_INVALID_STRING_LENGTH);
ReturnErrorOnFailure(tlvReader.Next());
}

Expand Down
65 changes: 62 additions & 3 deletions src/lib/core/OTAImageHeader.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
#pragma once

#include <lib/core/Optional.h>
#include <lib/support/ScopedBuffer.h>
#include <lib/support/Span.h>

#include <cstdint>

namespace chip {

/// File signature (aka magic number) of a valid Matter OTA image
constexpr uint32_t kOTAImageFileIdentifier = 0x1BEEF11E;

enum class OTAImageDigestType : uint8_t
Expand All @@ -44,8 +46,6 @@ enum class OTAImageDigestType : uint8_t

struct OTAImageHeader
{
uint64_t mTotalSize;
uint32_t mHeaderSize;
uint16_t mVendorId;
uint16_t mProductId;
uint32_t mSoftwareVersion;
Expand All @@ -58,6 +58,65 @@ struct OTAImageHeader
ByteSpan mImageDigest;
};

CHIP_ERROR DecodeOTAImageHeader(ByteSpan buffer, OTAImageHeader & header);
class OTAImageHeaderParser
{
public:
/**
* @brief Prepare the parser for accepting Matter OTA image chunks.
*
* The method can be called many times to reset the parser state.
*/
void Init();

/**
* @brief Clear all resources associated with the parser.
*/
void Clear();

/**
* @brief Returns if the parser is ready to accept subsequent Matter OTA image chunks.
*/
bool IsInitialized() const { return mState != State::kNotInitialized; }

/**
* @brief Decode Matter OTA image header
*
* The method takes subsequent chunks of the Matter OTA image file and decodes the header when
* enough data has been provided. If more image chunks are needed, CHIP_ERROR_BUFFER_TOO_SMALL
* error is returned. Other error codes indicate that the header is invalid.
*
* @param buffer Byte span containing a subsequent Matter OTA image chunk. When the method
* returns CHIP_NO_ERROR, the byte span is used to return a remaining part
* of the chunk, not used by the header.
* @param header Structure to store results of the operation. Note that the results must not be
* referenced after the parser is cleared since string members of the structure
* are only shallow-copied by the method.
*
* @retval CHIP_NO_ERROR Header has been decoded successfully.
* @retval CHIP_ERROR_BUFFER_TOO_SMALL Provided buffers are insufficient to decode the
* header. A user is expected call the method again
* when the next image chunk is available.
* @retval CHIP_ERROR_INVALID_FILE_IDENTIFIER Not a Matter OTA image file.
* @retval Error code Encoded header is invalid.
*/
CHIP_ERROR AccumulateAndDecode(ByteSpan & buffer, OTAImageHeader & header);

private:
enum State
{
kNotInitialized,
kInitialized,
kTlv
};

void Append(ByteSpan & buffer, uint32_t numBytes);
CHIP_ERROR DecodeFixed();
CHIP_ERROR DecodeTlv(OTAImageHeader & header);

State mState;
uint32_t mHeaderTlvSize;
uint32_t mBufferOffset;
Platform::ScopedMemoryBuffer<uint8_t> mBuffer;
};

} // namespace chip
Loading

0 comments on commit 8ce90ac

Please sign in to comment.