[SILABS] Multi-chip OTA work (#32349)

* Inital multi OTA work WIP

* fix wifi build

* cleanup and code review comments

* Restyled by clang-format

* Restyled by gn

* Restyled by gn

* code review comments

* Restyled by clang-format

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/examples/platform/silabs/OTAConfig.cpp b/examples/platform/silabs/OTAConfig.cpp
index adc6f23..6305af0 100644
--- a/examples/platform/silabs/OTAConfig.cpp
+++ b/examples/platform/silabs/OTAConfig.cpp
@@ -17,6 +17,7 @@
  */
 
 #include "OTAConfig.h"
+#include "silabs_utils.h"
 #include <app/server/Server.h>
 
 #ifndef SIWX_917
@@ -81,7 +82,6 @@
 chip::DefaultOTARequestorStorage gRequestorStorage;
 chip::DeviceLayer::DefaultOTARequestorDriver gRequestorUser;
 chip::BDXDownloader gDownloader;
-chip::OTAImageProcessorImpl gImageProcessor;
 
 void OTAConfig::Init()
 {
@@ -93,12 +93,23 @@
 
     // Periodic query timeout must be set prior to requestor being initialized
     gRequestorUser.SetPeriodicQueryTimeout(OTA_PERIODIC_TIMEOUT);
-    gRequestorUser.Init(&gRequestorCore, &gImageProcessor);
 
-    gImageProcessor.SetOTAImageFile("test.txt");
-    gImageProcessor.SetOTADownloader(&gDownloader);
+#if CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR
+    auto & imageProcessor = chip::OTAMultiImageProcessorImpl::GetDefaultInstance();
+#else
+    auto & imageProcessor = chip::OTAImageProcessorImpl::GetDefaultInstance();
+#endif
+
+    gRequestorUser.Init(&gRequestorCore, &imageProcessor);
+
+    CHIP_ERROR err = imageProcessor.Init(&gDownloader);
+    if (err != CHIP_NO_ERROR)
+    {
+        SILABS_LOG("Image processor init failed");
+        assert(err == CHIP_NO_ERROR);
+    }
 
     // Connect the Downloader and Image Processor objects
-    gDownloader.SetImageProcessorDelegate(&gImageProcessor);
+    gDownloader.SetImageProcessorDelegate(&imageProcessor);
     // Initialize and interconnect the Requestor and Image Processor objects -- END
 }
diff --git a/examples/platform/silabs/OTAConfig.h b/examples/platform/silabs/OTAConfig.h
index 2d5dbcf..2b7ed9a 100644
--- a/examples/platform/silabs/OTAConfig.h
+++ b/examples/platform/silabs/OTAConfig.h
@@ -22,7 +22,12 @@
 #include <app/clusters/ota-requestor/DefaultOTARequestor.h>
 #include <app/clusters/ota-requestor/DefaultOTARequestorDriver.h>
 #include <app/clusters/ota-requestor/DefaultOTARequestorStorage.h>
+
+#if CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR
+#include <platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h>
+#else
 #include <platform/silabs/OTAImageProcessorImpl.h>
+#endif
 
 class OTAConfig
 {
diff --git a/examples/platform/silabs/efr32/BUILD.gn b/examples/platform/silabs/efr32/BUILD.gn
index 02f1317..8b7b1eb 100644
--- a/examples/platform/silabs/efr32/BUILD.gn
+++ b/examples/platform/silabs/efr32/BUILD.gn
@@ -180,6 +180,10 @@
     defines += [ "HEAP_MONITORING" ]
   }
 
+  if (chip_enable_multi_ota_requestor) {
+    defines += [ "CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR=1" ]
+  }
+
   ldflags = [ "-Wl,--no-warn-rwx-segment" ]
 }
 
diff --git a/scripts/tools/silabs/ota/README.md b/scripts/tools/silabs/ota/README.md
new file mode 100644
index 0000000..d30dd45
--- /dev/null
+++ b/scripts/tools/silabs/ota/README.md
@@ -0,0 +1,71 @@
+---
+orphan: true
+---
+
+# Silabs OTA image tool
+
+## Overview
+
+This tool can generate an OTA image in the `|OTA standard header|TLV1|...|TLVn|`
+format. The payload contains data in standard TLV format (not Matter TLV
+format). During OTA transfer, these TLV can span across multiple BDX blocks,
+thus the `OTAImageProcessorImpl` instance should take this into account.
+
+## Supported platforms
+
+-   EFR32 -
+
+## Usage
+
+This is a wrapper over standard `ota_image_tool.py`, so the options for `create`
+are also available here:
+
+```
+python3 ./scripts/tools/silabs/ota/ota_image_tool.py create -v 0xDEAD -p 0xBEEF -vn 50000 -vs "1.0" -da sha256
+```
+
+followed by \*_custom options_- and a positional argument (should be last) that
+specifies the output file. Please see the `create_ota_images.sh` for some
+reference commands.
+
+The list of **custom options**:
+
+```
+# Application options
+--app-input-file   --> Path to the application binary.
+--app-version      --> Application version. It's part of the descriptor and
+                       can be different than the OTA image header version: -vn.
+--app-version-str  --> Application version string. Same as above.
+--app-build-date   --> Application build date. Same as above.
+
+# SSBL options
+--bl-input-file    --> Path to the SSBL binary.
+--bl-version       --> SSBL version.
+--bl-version-str   --> SSBL version string.
+--bl-build-date    --> SSBL build date.
+
+# Factory data options
+--factory-data     --> If set, enables the generation of factory data.
+--cert_declaration --> Certification Declaration.
+--dac_cert         --> DAC certificate.
+--dac_key          --> DAC private key.
+--pai_cert         --> PAI certificate.
+
+# Custom TLV options
+--json             --> Path to a JSON file following ota_payload.schema
+```
+
+Please note that the options above are separated into four categories:
+application, bootloader, factory data and custom TLV (`--json` option). If no
+descriptor options are specified for app/SSBL, the script will use the default
+values (`50000`, `"50000-default"`, `"2023-01-01"`). The descriptor feature is
+optional, TLV processors having the option to register a callback for descriptor
+processing.
+
+## Custom payload
+
+When defining a custom processor, a user is able to also specify the custom
+format of the TLV by creating a JSON file based on the `ota_payload.schema`. The
+tool offers support for describing multiple TLV in the same JSON file. Please
+see the `examples/ota_max_entries_example.json` for a multi-app + SSBL example.
+Option `--json` must be used to specify the path to the JSON file.
diff --git a/scripts/tools/silabs/ota/crypto_utils.py b/scripts/tools/silabs/ota/crypto_utils.py
new file mode 100755
index 0000000..dbab140
--- /dev/null
+++ b/scripts/tools/silabs/ota/crypto_utils.py
@@ -0,0 +1,487 @@
+#!/usr/bin/env python3
+#
+#    Copyright (c) 2023 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.
+#
+"""
+A pure python (slow) implementation of rijndael with a decent interface
+
+To include -
+
+from rijndael import rijndael
+
+To do a key setup -
+
+r = rijndael(key, block_size = 16)
+
+key must be a string of length 16, 24, or 32
+blocksize must be 16, 24, or 32. Default is 16
+
+To use -
+
+ciphertext = r.encrypt(plaintext)
+plaintext = r.decrypt(ciphertext)
+
+If any strings are of the wrong length a ValueError is thrown
+"""
+# ported from the Java reference code by Bram Cohen, April 2001
+# this code is public domain, unless someone makes
+# an intellectual property claim against the reference
+# code, in which case it can be made public domain by
+# deleting all the comments and renaming all the variables
+
+import copy
+import logging
+import struct
+
+shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]],
+          [[0, 0], [1, 5], [2, 4], [3, 3]],
+          [[0, 0], [1, 7], [3, 5], [4, 4]]]
+
+# [keysize][block_size]
+num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}}
+
+A = [[1, 1, 1, 1, 1, 0, 0, 0],
+     [0, 1, 1, 1, 1, 1, 0, 0],
+     [0, 0, 1, 1, 1, 1, 1, 0],
+     [0, 0, 0, 1, 1, 1, 1, 1],
+     [1, 0, 0, 0, 1, 1, 1, 1],
+     [1, 1, 0, 0, 0, 1, 1, 1],
+     [1, 1, 1, 0, 0, 0, 1, 1],
+     [1, 1, 1, 1, 0, 0, 0, 1]]
+
+# produce log and alog tables, needed for multiplying in the
+# field GF(2^m) (generator = 3)
+alog = [1]
+for i in range(255):
+    j = (alog[-1] << 1) ^ alog[-1]
+    if j & 0x100 != 0:
+        j ^= 0x11B
+    alog.append(j)
+
+log = [0] * 256
+for i in range(1, 255):
+    log[alog[i]] = i
+
+
+# multiply two elements of GF(2^m)
+def mul(a, b):
+    if a == 0 or b == 0:
+        return 0
+    return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255]  # noqa: F821
+
+
+# substitution box based on F^{-1}(x)
+box = [[0] * 8 for i in range(256)]
+box[1][7] = 1
+for i in range(2, 256):
+    j = alog[255 - log[i]]
+    for t in range(8):
+        box[i][t] = (j >> (7 - t)) & 0x01
+
+B = [0, 1, 1, 0, 0, 0, 1, 1]
+
+# affine transform:  box[i] <- B + A*box[i]
+cox = [[0] * 8 for i in range(256)]
+for i in range(256):
+    for t in range(8):
+        cox[i][t] = B[t]
+        for j in range(8):
+            cox[i][t] ^= A[t][j] * box[i][j]
+
+# S-boxes and inverse S-boxes
+S = [0] * 256
+Si = [0] * 256
+for i in range(256):
+    S[i] = cox[i][0] << 7
+    for t in range(1, 8):
+        S[i] ^= cox[i][t] << (7-t)
+    Si[S[i] & 0xFF] = i
+
+# T-boxes
+G = [[2, 1, 1, 3],
+     [3, 2, 1, 1],
+     [1, 3, 2, 1],
+     [1, 1, 3, 2]]
+
+AA = [[0] * 8 for i in range(4)]
+
+for i in range(4):
+    for j in range(4):
+        AA[i][j] = G[i][j]
+        AA[i][i+4] = 1
+
+for i in range(4):
+    pivot = AA[i][i]
+    if pivot == 0:
+        t = i + 1
+        while AA[t][i] == 0 and t < 4:
+            t += 1
+            assert t != 4, 'G matrix must be invertible'
+            for j in range(8):
+                AA[i][j], AA[t][j] = AA[t][j], AA[i][j]
+            pivot = AA[i][i]
+    for j in range(8):
+        if AA[i][j] != 0:
+            AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255]
+    for t in range(4):
+        if i != t:
+            for j in range(i+1, 8):
+                AA[t][j] ^= mul(AA[i][j], AA[t][i])
+            AA[t][i] = 0
+
+iG = [[0] * 4 for i in range(4)]
+
+for i in range(4):
+    for j in range(4):
+        iG[i][j] = AA[i][j + 4]
+
+
+def mul4(a, bs):
+    if a == 0:
+        return 0
+    r = 0
+    for b in bs:
+        r <<= 8
+        if b != 0:
+            r = r | mul(a, b)  # noqa: F821
+    return r
+
+
+T1 = []
+T2 = []
+T3 = []
+T4 = []
+T5 = []
+T6 = []
+T7 = []
+T8 = []
+U1 = []
+U2 = []
+U3 = []
+U4 = []
+
+for t in range(256):
+    s = S[t]
+    T1.append(mul4(s, G[0]))
+    T2.append(mul4(s, G[1]))
+    T3.append(mul4(s, G[2]))
+    T4.append(mul4(s, G[3]))
+
+    s = Si[t]
+    T5.append(mul4(s, iG[0]))
+    T6.append(mul4(s, iG[1]))
+    T7.append(mul4(s, iG[2]))
+    T8.append(mul4(s, iG[3]))
+
+    U1.append(mul4(t, iG[0]))
+    U2.append(mul4(t, iG[1]))
+    U3.append(mul4(t, iG[2]))
+    U4.append(mul4(t, iG[3]))
+
+# round constants
+rcon = [1]
+r = 1
+for t in range(1, 30):
+    r = mul(2, r)
+    rcon.append(r)
+
+del A
+del AA
+del pivot
+del B
+del G
+del box
+del log
+del alog
+del i
+del j
+del r
+del s
+del t
+del mul
+del mul4
+del cox
+del iG
+
+
+class rijndael:
+    def __init__(self, key, block_size=16):
+        if block_size != 16 and block_size != 24 and block_size != 32:
+            raise ValueError('Invalid block size: ' + str(block_size))
+        if len(key) != 16 and len(key) != 24 and len(key) != 32:
+            raise ValueError('Invalid key size: ' + str(len(key)))
+        self.block_size = block_size
+
+        ROUNDS = num_rounds[len(key)][block_size]
+        BC = int(block_size / 4)
+
+        # encryption round keys
+        Ke = [[0] * BC for i in range(ROUNDS + 1)]
+        # decryption round keys
+        Kd = [[0] * BC for i in range(ROUNDS + 1)]
+        ROUND_KEY_COUNT = (ROUNDS + 1) * BC
+        KC = int(len(key) / 4)
+
+        # copy user material bytes into temporary ints
+        tk = []
+        for i in range(0, KC):
+            tk.append((key[i * 4] << 24) | (key[i * 4 + 1] << 16) |
+                      (key[i * 4 + 2] << 8) | key[i * 4 + 3])
+
+        # copy values into round key arrays
+        t = 0
+        j = 0
+        while j < KC and t < ROUND_KEY_COUNT:
+            Ke[int(t / BC)][t % BC] = tk[j]
+            Kd[ROUNDS - int(t / BC)][t % BC] = tk[j]
+            j += 1
+            t += 1
+        tt = 0
+        rconpointer = 0
+        while t < ROUND_KEY_COUNT:
+            # extrapolate using phi (the round key evolution function)
+            tt = tk[KC - 1]
+            tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \
+                     (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \
+                     (S[tt & 0xFF] & 0xFF) << 8 ^ \
+                     (S[(tt >> 24) & 0xFF] & 0xFF) ^ \
+                     (rcon[rconpointer] & 0xFF) << 24
+            rconpointer += 1
+            if KC != 8:
+                for i in range(1, KC):
+                    tk[i] ^= tk[i-1]
+            else:
+                for i in range(1, KC / 2):
+                    tk[i] ^= tk[i-1]
+                tt = tk[KC / 2 - 1]
+                tk[KC / 2] ^= (S[tt & 0xFF] & 0xFF) ^ \
+                              (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \
+                              (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \
+                              (S[(tt >> 24) & 0xFF] & 0xFF) << 24
+                for i in range(KC / 2 + 1, KC):
+                    tk[i] ^= tk[i-1]
+            # copy values into round key arrays
+            j = 0
+            while j < KC and t < ROUND_KEY_COUNT:
+                Ke[int(t / BC)][t % BC] = tk[j]
+                Kd[ROUNDS - int(t / BC)][t % BC] = tk[j]
+                j += 1
+                t += 1
+        # inverse MixColumn where needed
+        for r in range(1, ROUNDS):
+            for j in range(BC):
+                tt = Kd[r][j]
+                Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \
+                    U2[(tt >> 16) & 0xFF] ^ \
+                    U3[(tt >> 8) & 0xFF] ^ \
+                    U4[tt & 0xFF]
+        self.Ke = Ke
+        self.Kd = Kd
+
+    def encrypt(self, plaintext):
+        if len(plaintext) != self.block_size:
+            raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext)))
+        Ke = self.Ke
+
+        BC = int(self.block_size / 4)
+        ROUNDS = len(Ke) - 1
+        if BC == 4:
+            SC = 0
+        elif BC == 6:
+            SC = 1
+        else:
+            SC = 2
+        s1 = shifts[SC][1][0]
+        s2 = shifts[SC][2][0]
+        s3 = shifts[SC][3][0]
+        a = [0] * BC
+        # temporary work array
+        t = []
+        # plaintext to ints + key
+        for i in range(BC):
+            t.append((ord(plaintext[i * 4]) << 24 |
+                      ord(plaintext[i * 4 + 1]) << 16 |
+                      ord(plaintext[i * 4 + 2]) << 8 |
+                      ord(plaintext[i * 4 + 3])) ^ Ke[0][i])
+        # apply round transforms
+        for r in range(1, ROUNDS):
+            for i in range(BC):
+                a[i] = (T1[(t[i] >> 24) & 0xFF] ^
+                        T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^
+                        T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^
+                        T4[t[(i + s3) % BC] & 0xFF]) ^ Ke[r][i]
+            t = copy.copy(a)
+        # last round is special
+        result = []
+        for i in range(BC):
+            tt = Ke[ROUNDS][i]
+            result.append((S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+            result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+            result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+            result.append((S[t[(i + s3) % BC] & 0xFF] ^ tt) & 0xFF)
+        return ''.join(list(map(chr, result)))
+
+    def decrypt(self, ciphertext):
+        if len(ciphertext) != self.block_size:
+            raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext)))
+        Kd = self.Kd
+
+        BC = int(self.block_size / 4)
+        ROUNDS = len(Kd) - 1
+        if BC == 4:
+            SC = 0
+        elif BC == 6:
+            SC = 1
+        else:
+            SC = 2
+        s1 = shifts[SC][1][1]
+        s2 = shifts[SC][2][1]
+        s3 = shifts[SC][3][1]
+        a = [0] * BC
+        # temporary work array
+        t = [0] * BC
+        # ciphertext to ints + key
+        for i in range(BC):
+            t[i] = (ord(ciphertext[i * 4]) << 24 |
+                    ord(ciphertext[i * 4 + 1]) << 16 |
+                    ord(ciphertext[i * 4 + 2]) << 8 |
+                    ord(ciphertext[i * 4 + 3])) ^ Kd[0][i]
+        # apply round transforms
+        for r in range(1, ROUNDS):
+            for i in range(BC):
+                a[i] = (T5[(t[i] >> 24) & 0xFF] ^
+                        T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^
+                        T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^
+                        T8[t[(i + s3) % BC] & 0xFF]) ^ Kd[r][i]
+            t = copy.copy(a)
+        # last round is special
+        result = []
+        for i in range(BC):
+            tt = Kd[ROUNDS][i]
+            result.append((Si[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+            result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+            result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+            result.append((Si[t[(i + s3) % BC] & 0xFF] ^ tt) & 0xFF)
+        return ''.join(map(chr, result))
+
+
+def encryptFlashData(nonce, key, data, imageLen):
+    encyptedBlock = ''
+    if (imageLen % 16) != 0:
+        for x in range(16 - (imageLen % 16)):
+            data = data + bytes([255])
+        imageLen = len(data)
+
+    r = rijndael(key, block_size=16)
+
+    for x in range(int(imageLen / 16)):
+        # use nonce value to create encrypted chunk
+        encryptNonce = ''
+        for i in nonce:
+            tempString = "%08x" % i
+            y = 0
+            while y < 8:
+                encryptNonce = encryptNonce + chr(int(tempString[y:y+2], 16))
+                y = y + 2
+        encChunk = r.encrypt(encryptNonce)
+
+        # increment the nonce value
+        if (nonce[3] == 0xffffffff):
+            nonce[3] = 0
+        else:
+            nonce[3] += 1
+
+        # xor encypted junk with data chunk
+        chunk = data[x*16:(x+1)*16]  # Read 16 byte chucks. 128 bits
+
+        lchunk = chunk
+        lencChunk = list(map(ord, encChunk))
+
+        loutChunk = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+        for i in range(16):
+            loutChunk[i] = lchunk[i] ^ lencChunk[i]
+            encyptedBlock = encyptedBlock + chr(lchunk[i] ^ lencChunk[i])
+
+    return (encyptedBlock)
+
+
+def aParsePassKeyString(sPassKey):
+    lstu32Passkey = [0, 0, 0, 0]
+
+    try:
+        lstStrPassKey = sPassKey.split(",")
+
+    except Exception:
+        sPassKey = "0x00000000, 0x00000000, 0x00000000, 0x00000000"
+        lstStrPassKey = sPassKey.split(",")
+
+    if len(lstStrPassKey) == 4:
+        for i in range(4):
+            if "0x" in lstStrPassKey[i]:
+                lstu32Passkey[i] = int(lstStrPassKey[i], 16)
+            else:
+                lstu32Passkey[i] = int(lstStrPassKey[i], 10)
+
+    logging.info(f"\t-key: {lstu32Passkey[0]}, {lstu32Passkey[1]}, {lstu32Passkey[2]}, {lstu32Passkey[3]}")
+    abEncryptKey = struct.pack(">LLLL", lstu32Passkey[0],
+                               lstu32Passkey[1],
+                               lstu32Passkey[2],
+                               lstu32Passkey[3])
+    return abEncryptKey
+
+
+def aParseNonce(sNonceValue):
+    lstu32Nonce = [0, 0, 0, 0]
+
+    try:
+        lstStrNonce = sNonceValue.split(",")
+
+    except Exception:
+        sNonceValue = "0x00000000, 0x00000000, 0x00000000, 0x00000000"
+        lstStrNonce = sNonceValue.split(",")
+
+    if len(lstStrNonce) == 4:
+        for i in range(4):
+            if "0x" in lstStrNonce[i]:
+                lstu32Nonce[i] = int(lstStrNonce[i], 16)
+            else:
+                lstu32Nonce[i] = int(lstStrNonce[i], 10)
+
+    logging.info(f"Nonce : {lstu32Nonce[0]}, {lstu32Nonce[1]}, {lstu32Nonce[2]}, {lstu32Nonce[3]}")
+
+    return lstu32Nonce
+
+
+def encryptData(sSrcData, sPassKey, aPassIv):
+
+    sKeyString = sPassKey.strip()
+    assert len(sKeyString) == 32, 'the length of encryption key should be equal to 32'
+    sPassString = "0x" + sKeyString[:8] + ',' + "0x" + sKeyString[8:16] + \
+        ',' + "0x" + sKeyString[16:24] + ',' + "0x" + sKeyString[24:32]
+    aPassKey = aParsePassKeyString(sPassString)
+
+    sIvString = aPassIv.strip()
+    sPassString = "0x" + sIvString[:8] + ',' + "0x" + sIvString[8:16] + \
+        ',' + "0x" + sIvString[16:24] + ',' + "0x" + sIvString[24:32]
+    aNonce = aParseNonce(sPassString)
+
+    logging.info("Started Encrypting with key[{}] ......".format(sPassKey))
+
+    encryptedData = encryptFlashData(aNonce, aPassKey, sSrcData, len(sSrcData))
+
+    logging.info("Done")
+
+    return encryptedData
diff --git a/scripts/tools/silabs/ota/ota_image_tool.py b/scripts/tools/silabs/ota/ota_image_tool.py
new file mode 100755
index 0000000..64715d7
--- /dev/null
+++ b/scripts/tools/silabs/ota/ota_image_tool.py
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+#
+#    Copyright (c) 2023 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.
+#
+
+'''This file should contain a way to generate custom OTA payloads.
+
+The format of the custom payload is the following:
+| Total size of TLVs | TLV1 | ... | TLVn |
+
+The OTA payload can then be used to generate an OTA image file, which
+will be parsed by the OTA image processor. The total size of TLVs is
+needed as input for a TLVReader.
+
+Currently, this script only supports Certification Declaration update,
+but it could be modified to support all factory data fields.
+'''
+
+import argparse
+import glob
+import json
+import logging
+import os
+import sys
+
+import crypto_utils
+import jsonschema
+
+sys.path.insert(0, os.path.join(
+    os.path.dirname(__file__), '../factory_data_generator'))
+sys.path.insert(0, os.path.join(
+    os.path.dirname(__file__), '../../../../src/controller/python'))
+sys.path.insert(0, os.path.join(
+    os.path.dirname(__file__), '../../../../src/app/'))
+
+import ota_image_tool  # noqa: E402 isort:skip
+from chip.tlv import TLVWriter  # noqa: E402 isort:skip
+from custom import CertDeclaration, DacCert, DacPKey, PaiCert  # noqa: E402 isort:skip
+from default import InputArgument  # noqa: E402 isort:skip
+from generate import set_logger  # noqa: E402 isort:skip
+
+OTA_APP_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_app_tlv.bin")
+OTA_BOOTLOADER_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_ssbl_tlv.bin")
+OTA_FACTORY_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_factory_tlv.bin")
+
+INITIALIZATION_VECTOR = "00000010111213141516171800000000"
+
+
+class TAG:
+    APPLICATION = 1
+    BOOTLOADER = 2
+    FACTORY_DATA = 3
+
+
+def write_to_temp(path: str, payload: bytearray):
+    with open(path, "wb") as _handle:
+        _handle.write(payload)
+
+    logging.info(f"Data payload size for {path.split('/')[-1]}: {len(payload)}")
+
+
+def generate_header(tag: int, length: int):
+    header = bytearray(tag.to_bytes(4, "little"))
+    header += bytearray(length.to_bytes(4, "little"))
+    return header
+
+
+def generate_factory_data(args: object):
+    """
+    Generate custom OTA payload from InputArgument derived objects. The payload is
+    written in a temporary file that will be appended to args.input_files.
+    """
+    fields = dict()
+
+    if args.dac_key is not None:
+        args.dac_key.generate_private_key(args.dac_key_password)
+
+    data = [obj for key, obj in vars(args).items() if isinstance(obj, InputArgument)]
+    for arg in sorted(data, key=lambda x: x.key()):
+        fields.update({arg.key(): arg.encode()})
+
+    if fields:
+        writer = TLVWriter()
+        writer.put(None, fields)
+        logging.info(f"factory data encryption enable: {args.enc_enable}")
+        if args.enc_enable:
+            enc_factory_data = crypto_utils.encryptData(writer.encoding, args.input_ota_key, INITIALIZATION_VECTOR)
+            enc_factory_data1 = bytes([ord(x) for x in enc_factory_data])
+            payload = generate_header(TAG.FACTORY_DATA, len(enc_factory_data1))
+            payload += enc_factory_data1
+        else:
+            payload = generate_header(TAG.FACTORY_DATA, len(writer.encoding))
+            payload += writer.encoding
+
+    write_to_temp(OTA_FACTORY_TLV_TEMP, payload)
+
+    return [OTA_FACTORY_TLV_TEMP]
+
+
+def generate_descriptor(version: int, versionStr: str, buildDate: str):
+    """
+    Generate descriptor as bytearray for app/SSBL payload.
+    """
+    v = version if version is not None else 50000
+    vs = versionStr if versionStr is not None else "50000-default"
+    bd = buildDate if buildDate is not None else "2023-01-01"
+
+    logging.info(f"\t-version: {v}")
+    logging.info(f"\t-version str: {vs}")
+    logging.info(f"\t-build date: {bd}")
+
+    v = v.to_bytes(4, "little")
+    vs = bytearray(vs, "ascii") + bytearray(64 - len(vs))
+    bd = bytearray(bd, "ascii") + bytearray(64 - len(bd))
+
+    return v + vs + bd
+
+
+def generate_app(args: object):
+    """
+    Generate app payload with descriptor. If a certain option is not specified, use the default values.
+    """
+    logging.info("App descriptor information:")
+
+    descriptor = generate_descriptor(args.app_version, args.app_version_str, args.app_build_date)
+    logging.info(f"App encryption enable: {args.enc_enable}")
+    if args.enc_enable:
+        inputFile = open(args.app_input_file, "rb")
+        enc_file = crypto_utils.encryptData(inputFile.read(), args.input_ota_key, INITIALIZATION_VECTOR)
+        enc_file1 = bytes([ord(x) for x in enc_file])
+        file_size = len(enc_file1)
+        payload = generate_header(TAG.APPLICATION, len(descriptor) + file_size) + descriptor + enc_file1
+    else:
+        file_size = os.path.getsize(args.app_input_file)
+        logging.info(f"file size: {file_size}")
+        payload = generate_header(TAG.APPLICATION, len(descriptor) + file_size) + descriptor
+
+    write_to_temp(OTA_APP_TLV_TEMP, payload)
+    if args.enc_enable:
+        return [OTA_APP_TLV_TEMP]
+    else:
+        return [OTA_APP_TLV_TEMP, args.app_input_file]
+
+
+def generate_bootloader(args: object):
+    """
+    Generate SSBL payload with descriptor. If a certain option is not specified, use the default values.
+    """
+    logging.info("SSBL descriptor information:")
+
+    descriptor = generate_descriptor(args.bl_version, args.bl_version_str, args.bl_build_date)
+    logging.info(f"Bootloader encryption enable: {args.enc_enable}")
+    if args.enc_enable:
+        inputFile = open(args.bl_input_file, "rb")
+        enc_file = crypto_utils.encryptData(inputFile.read(), args.input_ota_key, INITIALIZATION_VECTOR)
+        enc_file1 = bytes([ord(x) for x in enc_file])
+        file_size = len(enc_file1)
+        payload = generate_header(TAG.BOOTLOADER, len(descriptor) + file_size) + descriptor + enc_file1
+    else:
+        file_size = os.path.getsize(args.bl_input_file)
+        logging.info(f"file size: {file_size}")
+        payload = generate_header(TAG.BOOTLOADER, len(descriptor) + file_size) + descriptor
+
+    write_to_temp(OTA_BOOTLOADER_TLV_TEMP, payload)
+    if args.enc_enable:
+        return [OTA_BOOTLOADER_TLV_TEMP]
+    else:
+        return [OTA_BOOTLOADER_TLV_TEMP, args.bl_input_file]
+
+
+def validate_json(data: str):
+    with open(os.path.join(os.path.dirname(__file__), 'ota_payload.schema'), 'r') as fd:
+        payload_schema = json.load(fd)
+
+    try:
+        jsonschema.validate(instance=data, schema=payload_schema)
+        logging.info("JSON data is valid")
+    except jsonschema.exceptions.ValidationError as err:
+        logging.error(f"JSON data is invalid: {err}")
+        sys.exit(1)
+
+
+def generate_custom_tlvs(data):
+    """
+    Generate custom OTA payload from a JSON object following a predefined schema.
+    The payload is written in a temporary file that will be appended to args.input_files.
+    """
+    input_files = []
+
+    payload = bytearray()
+    descriptor = bytearray()
+    iteration = 0
+    for entry in data["inputs"]:
+        if "descriptor" in entry:
+            for field in entry["descriptor"]:
+                if isinstance(field["value"], str):
+                    descriptor += bytearray(field["value"], "ascii") + bytearray(field["length"] - len(field["value"]))
+                elif isinstance(field["value"], int):
+                    descriptor += bytearray(field["value"].to_bytes(field["length"], "little"))
+        file_size = os.path.getsize(entry["path"])
+        payload = generate_header(entry["tag"], len(descriptor) + file_size) + descriptor
+
+        temp_output = os.path.join(os.path.dirname(__file__), "ota_temp_custom_tlv_" + str(iteration) + ".bin")
+        write_to_temp(temp_output, payload)
+
+        input_files += [temp_output, entry["path"]]
+        iteration += 1
+        descriptor = bytearray()
+
+    return input_files
+
+
+def show_payload(args: object):
+    """
+    Parse and present OTA custom payload in human-readable form.
+    """
+    # TODO: implement to show current TLVs
+    pass
+
+
+def create_image(args: object):
+    ota_image_tool.validate_header_attributes(args)
+
+    input_files = list()
+
+    if args.json:
+        with open(args.json, 'r') as fd:
+            data = json.load(fd)
+        validate_json(data)
+        input_files += generate_custom_tlvs(data)
+
+    if args.factory_data:
+        input_files += generate_factory_data(args)
+
+    if args.bl_input_file:
+        input_files += generate_bootloader(args)
+
+    if args.app_input_file:
+        input_files += generate_app(args)
+
+    if len(input_files) == 0:
+        print("Please specify an input option.")
+        sys.exit(1)
+
+    logging.info("Input files used:")
+    [logging.info(f"\t- {_file}") for _file in input_files]
+
+    args.input_files = input_files
+    ota_image_tool.generate_image(args)
+
+    for filename in glob.glob(os.path.dirname(__file__) + "/ota_temp_*"):
+        os.remove(filename)
+    if args.enc_enable:
+        for filename in glob.glob(os.path.dirname(__file__) + "/enc_ota_temp_*"):
+            os.remove(filename)
+
+
+def main():
+    """
+    This function is a modified version of ota_image_tool.py main function.
+
+    The wrapper version defines a new set of args, which are used to generate
+    TLV data that will be embedded in the final OTA payload.
+    """
+
+    def any_base_int(s): return int(s, 0)
+
+    set_logger()
+    parser = argparse.ArgumentParser(
+        description='Matter OTA (Over-the-air update) image utility', fromfile_prefix_chars='@')
+    subcommands = parser.add_subparsers(
+        dest='subcommand', title='valid subcommands', required=True)
+
+    create_parser = subcommands.add_parser('create', help='Create OTA image')
+    create_parser.add_argument('-v', '--vendor-id', type=any_base_int,
+                               required=True, help='Vendor ID')
+    create_parser.add_argument('-p', '--product-id', type=any_base_int,
+                               required=True, help='Product ID')
+    create_parser.add_argument('-vn', '--version', type=any_base_int,
+                               required=True, help='Software version (numeric)')
+    create_parser.add_argument('-vs', '--version-str', required=True,
+                               help='Software version (string)')
+    create_parser.add_argument('-da', '--digest-algorithm', choices=ota_image_tool.DIGEST_ALL_ALGORITHMS,
+                               required=True, help='Digest algorithm')
+    create_parser.add_argument('-mi', '--min-version', type=any_base_int,
+                               help='Minimum software version that can be updated to this image')
+    create_parser.add_argument('-ma', '--max-version', type=any_base_int,
+                               help='Maximum software version that can be updated to this image')
+    create_parser.add_argument('-rn', '--release-notes',
+                               help='Release note URL')
+
+    create_parser.add_argument('-app', "--app-input-file",
+                               help='Path to application input file')
+    create_parser.add_argument('--app-version', type=any_base_int,
+                               help='Application Software version (numeric)')
+    create_parser.add_argument('--app-version-str', type=str,
+                               help='Application Software version (string)')
+    create_parser.add_argument('--app-build-date', type=str,
+                               help='Application build date (string)')
+
+    create_parser.add_argument('-bl', '--bl-input-file',
+                               help='Path to input bootloader image payload file')
+    create_parser.add_argument('--bl-version', type=any_base_int,
+                               help='Bootloader Software version (numeric)')
+    create_parser.add_argument('--bl-version-str', type=str,
+                               help='Bootloader Software version (string)')
+    create_parser.add_argument('--bl-build-date', type=str,
+                               help='Bootloader build date (string)')
+
+    # Factory data specific arguments. Will be used to generate the TLV payload.
+    create_parser.add_argument('-fd', '--factory-data', action='store_true',
+                               help='If found, enable factory data payload generation.')
+    create_parser.add_argument("--cert_declaration", type=CertDeclaration,
+                               help="[path] Path to Certification Declaration in DER format")
+    create_parser.add_argument("--dac_cert", type=DacCert,
+                               help="[path] Path to DAC certificate in DER format")
+    create_parser.add_argument("--dac_key", type=DacPKey,
+                               help="[path] Path to DAC key in DER format")
+    create_parser.add_argument("--dac_key_password", type=str,
+                               help="[path] Password to decode DAC Key if available")
+    create_parser.add_argument("--pai_cert", type=PaiCert,
+                               help="[path] Path to PAI certificate in DER format")
+
+    # Path to input JSON file which describes custom TLVs.
+    create_parser.add_argument('--json', help="[path] Path to the JSON describing custom TLVs")
+
+    create_parser.add_argument('--enc_enable', action="store_true", help='enable ota encryption')
+    create_parser.add_argument('--input_ota_key', type=str, default="1234567890ABCDEFA1B2C3D4E5F6F1B4",
+                               help='Input OTA Encryption KEY (string:16Bytes)')
+
+    create_parser.add_argument('-i', '--input_files', default=list(),
+                               help='Path to input image payload file')
+    create_parser.add_argument('output_file', help='Path to output image file')
+
+    show_parser = subcommands.add_parser('show', help='Show OTA image info')
+    show_parser.add_argument('image_file', help='Path to OTA image file')
+
+    extract_tool = subcommands.add_parser('extract', help='Remove the OTA header from an image file')
+    extract_tool.add_argument('image_file', help='Path to OTA image file with header')
+    extract_tool.add_argument('output_file', help='Path to put the output file (no header)')
+
+    change_tool = subcommands.add_parser('change_header', help='Change the specified values in the header')
+    change_tool.add_argument('-v', '--vendor-id', type=any_base_int,
+                             help='Vendor ID')
+    change_tool.add_argument('-p', '--product-id', type=any_base_int,
+                             help='Product ID')
+    change_tool.add_argument('-vn', '--version', type=any_base_int,
+                             help='Software version (numeric)')
+    change_tool.add_argument('-vs', '--version-str',
+                             help='Software version (string)')
+    change_tool.add_argument('-da', '--digest-algorithm', choices=ota_image_tool.DIGEST_ALL_ALGORITHMS,
+                             help='Digest algorithm')
+    change_tool.add_argument('-mi', '--min-version', type=any_base_int,
+                             help='Minimum software version that can be updated to this image')
+    change_tool.add_argument('-ma', '--max-version', type=any_base_int,
+                             help='Maximum software version that can be updated to this image')
+    change_tool.add_argument(
+        '-rn', '--release-notes', help='Release note URL')
+    change_tool.add_argument('image_file',
+                             help='Path to input OTA file')
+    change_tool.add_argument('output_file', help='Path to output OTA file')
+
+    args = parser.parse_args()
+
+    if args.subcommand == 'create':
+        create_image(args)
+    elif args.subcommand == 'show':
+        ota_image_tool.show_header(args)
+        show_payload(args)
+    elif args.subcommand == 'extract':
+        ota_image_tool.remove_header(args)
+    elif args.subcommand == 'change_header':
+        ota_image_tool.update_header_args(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/tools/silabs/ota/ota_payload.schema b/scripts/tools/silabs/ota/ota_payload.schema
new file mode 100644
index 0000000..bceacf7
--- /dev/null
+++ b/scripts/tools/silabs/ota/ota_payload.schema
@@ -0,0 +1,67 @@
+{
+    "$schema": "https://json-schema.org/draft/2020-12/schema",
+    "$id": "Custom_OTA_TLV_schema",
+    "description": "A representation of custom OTA payload with variable number of TLVs",
+    "type": "object",
+    "required": [
+        "inputs"
+    ],
+    "properties": {
+        "inputs": {
+            "type": "array",
+            "items": {
+                "type": "object",
+                "required": [
+                    "tag",
+                    "path"
+                ],
+                "properties": {
+                    "tag": {
+                        "type": "integer",
+                        "description": "TLV's tag value used to select a parser"
+                    },
+                    "descriptor": {
+                        "type": "array",
+                        "description": "Metadata of the TLV value field (C struct)",
+                        "items": {
+                            "$ref": "#/$defs/field"
+                        }
+                    },
+                    "path": {
+                        "type": "string",
+                        "description": "System path to the binary"
+                    }
+                }
+            }
+        }
+    },
+    "$defs": {
+        "field": {
+            "type": "object",
+            "required": [
+                "name",
+                "length",
+                "value"
+            ],
+            "properties": {
+                "name": {
+                    "type": "string"
+                },
+                "length": {
+                    "type": "integer",
+                    "description": "Number of bytes occupied in memory"
+                },
+                "value": {
+                    "anyOf": [
+                        {
+                            "type": "string"
+                        },
+                        {
+                            "type": "integer"
+                        }
+                    ]
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/platform/silabs/OTAImageProcessorImpl.h b/src/platform/silabs/OTAImageProcessorImpl.h
index 46b78b0..5598b67 100644
--- a/src/platform/silabs/OTAImageProcessorImpl.h
+++ b/src/platform/silabs/OTAImageProcessorImpl.h
@@ -40,7 +40,8 @@
     CHIP_ERROR ConfirmCurrentImage() override;
 
     void SetOTADownloader(OTADownloader * downloader) { mDownloader = downloader; }
-    void SetOTAImageFile(const char * imageFile) { mImageFile = imageFile; }
+    CHIP_ERROR Init(OTADownloader * downloader);
+    static OTAImageProcessorImpl & GetDefaultInstance();
 
 private:
     //////////// Actual handlers for the OTAImageProcessorInterface ///////////////
@@ -68,7 +69,6 @@
     MutableByteSpan mBlock;
     OTADownloader * mDownloader;
     OTAImageHeaderParser mHeaderParser;
-    const char * mImageFile                 = nullptr;
     static constexpr size_t kAlignmentBytes = 64;
     // Intermediate, word-aligned buffer for writing to the bootloader storage.
     // Bootloader storage API requires the buffer size to be a multiple of 4.
diff --git a/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp b/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp
index 7e58113..d641bfa 100644
--- a/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp
+++ b/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp
@@ -38,6 +38,7 @@
 
 #define SL_STATUS_FW_UPDATE_DONE SL_STATUS_SI91X_NO_AP_FOUND
 uint8_t flag = RPS_HEADER;
+static chip::OTAImageProcessorImpl gImageProcessor;
 
 namespace chip {
 
@@ -48,6 +49,15 @@
 uint16_t OTAImageProcessorImpl::writeBufOffset                                          = 0;
 uint8_t OTAImageProcessorImpl::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 };
 
+CHIP_ERROR OTAImageProcessorImpl::Init(OTADownloader * downloader)
+{
+    ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    gImageProcessor.SetOTADownloader(downloader);
+
+    return CHIP_NO_ERROR;
+}
+
 CHIP_ERROR OTAImageProcessorImpl::PrepareDownload()
 {
     DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast<intptr_t>(this));
@@ -386,4 +396,9 @@
     return CHIP_NO_ERROR;
 }
 
+OTAImageProcessorImpl & OTAImageProcessorImpl::GetDefaultInstance()
+{
+    return gImageProcessor;
+}
+
 } // namespace chip
diff --git a/src/platform/silabs/efr32/BUILD.gn b/src/platform/silabs/efr32/BUILD.gn
index 83198d1..25b8e9f 100644
--- a/src/platform/silabs/efr32/BUILD.gn
+++ b/src/platform/silabs/efr32/BUILD.gn
@@ -17,6 +17,7 @@
 import("${chip_root}/build/chip/buildconfig_header.gni")
 import("${chip_root}/src/crypto/crypto.gni")
 import("${chip_root}/src/platform/device.gni")
+import("${chip_root}/third_party/silabs/efr32_sdk.gni")
 import("${chip_root}/third_party/silabs/silabs_board.gni")
 
 silabs_platform_dir = "${chip_root}/src/platform/silabs"
@@ -77,7 +78,17 @@
     sources += [ "BLEManagerImpl.cpp" ]
   }
 
-  if (chip_enable_ota_requestor) {
+  if (chip_enable_multi_ota_requestor) {
+    sources += [
+      "${silabs_platform_dir}/multi-ota/OTAMultiImageProcessorImpl.cpp",
+      "${silabs_platform_dir}/multi-ota/OTAMultiImageProcessorImpl.h",
+      "${silabs_platform_dir}/multi-ota/OTATlvProcessor.cpp",
+      "${silabs_platform_dir}/multi-ota/OTATlvProcessor.h",
+      "${silabs_platform_dir}/multi-ota/efr32/OTAFirmwareProcessor.cpp",
+      "${silabs_platform_dir}/multi-ota/efr32/OTAFirmwareProcessor.h",
+      "${silabs_platform_dir}/multi-ota/efr32/OTAHooks.cpp",
+    ]
+  } else if (chip_enable_ota_requestor) {
     sources += [
       "${silabs_platform_dir}/OTAImageProcessorImpl.h",
       "OTAImageProcessorImpl.cpp",
diff --git a/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp b/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp
index a2544f8..598ee5a 100644
--- a/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp
+++ b/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp
@@ -33,6 +33,8 @@
 /// No error, operation OK
 #define SL_BOOTLOADER_OK 0L
 
+static chip::OTAImageProcessorImpl gImageProcessor;
+
 namespace chip {
 
 // Define static memebers
@@ -41,6 +43,15 @@
 uint16_t OTAImageProcessorImpl::writeBufOffset                                          = 0;
 uint8_t OTAImageProcessorImpl::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 };
 
+CHIP_ERROR OTAImageProcessorImpl::Init(OTADownloader * downloader)
+{
+    ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    gImageProcessor.SetOTADownloader(downloader);
+
+    return CHIP_NO_ERROR;
+}
+
 CHIP_ERROR OTAImageProcessorImpl::PrepareDownload()
 {
     DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast<intptr_t>(this));
@@ -400,4 +411,9 @@
     return CHIP_NO_ERROR;
 }
 
+OTAImageProcessorImpl & OTAImageProcessorImpl::GetDefaultInstance()
+{
+    return gImageProcessor;
+}
+
 } // namespace chip
diff --git a/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp
new file mode 100644
index 0000000..147dcda
--- /dev/null
+++ b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp
@@ -0,0 +1,457 @@
+/*
+ *
+ *    Copyright (c) 2021-2023 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.
+ */
+
+#include <app/clusters/ota-requestor/OTADownloader.h>
+#include <app/clusters/ota-requestor/OTARequestorInterface.h>
+#include <lib/support/BufferReader.h>
+#include <platform/DiagnosticDataProvider.h>
+#include <platform/internal/CHIPDeviceLayerInternal.h>
+#include <platform/internal/GenericConfigurationManagerImpl.h>
+
+#include <platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h>
+
+using namespace chip::DeviceLayer;
+using namespace ::chip::DeviceLayer::Internal;
+
+static chip::OTAMultiImageProcessorImpl gImageProcessor;
+
+extern "C" {
+#include "btl_interface.h"
+#include "em_bus.h" // For CORE_CRITICAL_SECTION
+#if SL_WIFI
+#include "spi_multiplex.h"
+#endif // SL_WIFI
+}
+
+namespace chip {
+
+CHIP_ERROR OTAMultiImageProcessorImpl::Init(OTADownloader * downloader)
+{
+    ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    gImageProcessor.SetOTADownloader(downloader);
+
+    OtaHookInit();
+
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::Clear()
+{
+    mHeaderParser.Clear();
+    mAccumulator.Clear();
+    mParams.totalFileBytes  = 0;
+    mParams.downloadedBytes = 0;
+    mCurrentProcessor       = nullptr;
+
+    ReleaseBlock();
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::PrepareDownload()
+{
+    DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast<intptr_t>(this));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::Finalize()
+{
+    DeviceLayer::PlatformMgr().ScheduleWork(HandleFinalize, reinterpret_cast<intptr_t>(this));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::Apply()
+{
+    DeviceLayer::PlatformMgr().ScheduleWork(HandleApply, reinterpret_cast<intptr_t>(this));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::Abort()
+{
+    DeviceLayer::PlatformMgr().ScheduleWork(HandleAbort, reinterpret_cast<intptr_t>(this));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ProcessBlock(ByteSpan & block)
+{
+    if ((block.data() == nullptr) || block.empty())
+    {
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    // Store block data for HandleProcessBlock to access
+    CHIP_ERROR err = SetBlock(block);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(SoftwareUpdate, "Cannot set block data: %" CHIP_ERROR_FORMAT, err.Format());
+    }
+
+    DeviceLayer::PlatformMgr().ScheduleWork(HandleProcessBlock, reinterpret_cast<intptr_t>(this));
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::HandlePrepareDownload(intptr_t context)
+{
+    auto * imageProcessor = reinterpret_cast<OTAMultiImageProcessorImpl *>(context);
+
+    VerifyOrReturn(imageProcessor != nullptr, ChipLogError(SoftwareUpdate, "ImageProcessor context is null"));
+
+    VerifyOrReturn(imageProcessor->mDownloader != nullptr, ChipLogError(SoftwareUpdate, "mDownloader is null"));
+
+    ChipLogProgress(SoftwareUpdate, "HandlePrepareDownload: started");
+
+    CORE_CRITICAL_SECTION(bootloader_init();)
+
+    imageProcessor->mParams.downloadedBytes = 0;
+
+    imageProcessor->mHeaderParser.Init();
+    imageProcessor->mAccumulator.Init(sizeof(OTATlvHeader));
+    imageProcessor->mDownloader->OnPreparedForDownload(CHIP_NO_ERROR);
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ProcessHeader(ByteSpan & block)
+{
+    OTAImageHeader header;
+    ReturnErrorOnFailure(mHeaderParser.AccumulateAndDecode(block, header));
+
+    mParams.totalFileBytes = header.mPayloadSize;
+    mHeaderParser.Clear();
+    ChipLogError(SoftwareUpdate, "Processed header successfully");
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ProcessPayload(ByteSpan & block)
+{
+    CHIP_ERROR status = CHIP_NO_ERROR;
+
+    while (true)
+    {
+        if (!mCurrentProcessor)
+        {
+            ReturnErrorOnFailure(mAccumulator.Accumulate(block));
+            ByteSpan tlvHeader{ mAccumulator.data(), sizeof(OTATlvHeader) };
+            ReturnErrorOnFailure(SelectProcessor(tlvHeader));
+            ReturnErrorOnFailure(mCurrentProcessor->Init());
+        }
+
+        status = mCurrentProcessor->Process(block);
+        if (status == CHIP_OTA_CHANGE_PROCESSOR)
+        {
+            mAccumulator.Clear();
+            mAccumulator.Init(sizeof(OTATlvHeader));
+
+            mCurrentProcessor = nullptr;
+
+            // If the block size is 0, it means that the processed data was a multiple of
+            // received BDX block size (e.g. 8 blocks of 1024 bytes were transferred).
+            // After state for selecting next processor is reset, a request for fetching next
+            // data must be sent.
+            if (block.size() == 0)
+            {
+                status = CHIP_NO_ERROR;
+                break;
+            }
+        }
+        else
+        {
+            break;
+        }
+    }
+
+    return status;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::SelectProcessor(ByteSpan & block)
+{
+    OTATlvHeader header;
+    Encoding::LittleEndian::Reader reader(block.data(), sizeof(header));
+
+    ReturnErrorOnFailure(reader.Read32(&header.tag).StatusCode());
+    ReturnErrorOnFailure(reader.Read32(&header.length).StatusCode());
+
+    auto pair = mProcessorMap.find(header.tag);
+    if (pair == mProcessorMap.end())
+    {
+        ChipLogError(SoftwareUpdate, "There is no registered processor for tag: %lu", header.tag);
+        return CHIP_OTA_PROCESSOR_NOT_REGISTERED;
+    }
+
+    ChipLogDetail(SoftwareUpdate, "Selected processor with tag: %lu", pair->first);
+    mCurrentProcessor = pair->second;
+    mCurrentProcessor->SetLength(header.length);
+    mCurrentProcessor->SetWasSelected(true);
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::RegisterProcessor(uint32_t tag, OTATlvProcessor * processor)
+{
+    auto pair = mProcessorMap.find(tag);
+    if (pair != mProcessorMap.end())
+    {
+        ChipLogError(SoftwareUpdate, "A processor for tag %lu is already registered.", tag);
+        return CHIP_OTA_PROCESSOR_ALREADY_REGISTERED;
+    }
+
+    mProcessorMap.insert({ tag, processor });
+
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::HandleAbort(intptr_t context)
+{
+    ChipLogError(SoftwareUpdate, "OTA was aborted");
+    auto * imageProcessor = reinterpret_cast<OTAMultiImageProcessorImpl *>(context);
+    if (imageProcessor != nullptr)
+    {
+        imageProcessor->AbortAllProcessors();
+    }
+    imageProcessor->Clear();
+}
+
+void OTAMultiImageProcessorImpl::HandleProcessBlock(intptr_t context)
+{
+    auto * imageProcessor = reinterpret_cast<OTAMultiImageProcessorImpl *>(context);
+
+    VerifyOrReturn(imageProcessor != nullptr, ChipLogError(SoftwareUpdate, "ImageProcessor context is null"));
+
+    VerifyOrReturn(imageProcessor->mDownloader != nullptr, ChipLogError(SoftwareUpdate, "mDownloader is null"));
+
+    CHIP_ERROR status;
+    auto block = ByteSpan(imageProcessor->mBlock.data(), imageProcessor->mBlock.size());
+
+    if (imageProcessor->mHeaderParser.IsInitialized())
+    {
+        status = imageProcessor->ProcessHeader(block);
+        if (status != CHIP_NO_ERROR)
+        {
+            imageProcessor->HandleStatus(status);
+        }
+    }
+
+    status = imageProcessor->ProcessPayload(block);
+    imageProcessor->HandleStatus(status);
+}
+
+void OTAMultiImageProcessorImpl::HandleStatus(CHIP_ERROR status)
+{
+    if (status == CHIP_NO_ERROR || status == CHIP_ERROR_BUFFER_TOO_SMALL)
+    {
+        mParams.downloadedBytes += mBlock.size();
+        FetchNextData(0);
+    }
+    else if (status == CHIP_OTA_FETCH_ALREADY_SCHEDULED)
+    {
+        mParams.downloadedBytes += mBlock.size();
+    }
+    else
+    {
+        ChipLogError(SoftwareUpdate, "Image update canceled. Failed to process OTA block: %s", ErrorStr(status));
+        GetRequestorInstance()->CancelImageUpdate();
+    }
+}
+
+void OTAMultiImageProcessorImpl::AbortAllProcessors()
+{
+    ChipLogError(SoftwareUpdate, "All selected processors will call abort action");
+
+    for (auto const & pair : mProcessorMap)
+    {
+        if (pair.second->WasSelected())
+        {
+            pair.second->Clear();
+            pair.second->SetWasSelected(false);
+        }
+    }
+}
+
+bool OTAMultiImageProcessorImpl::IsFirstImageRun()
+{
+    OTARequestorInterface * requestor = chip::GetRequestorInstance();
+    if (requestor == nullptr)
+    {
+        return false;
+    }
+
+    return requestor->GetCurrentUpdateState() == OTARequestorInterface::OTAUpdateStateEnum::kApplying;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ConfirmCurrentImage()
+{
+    uint32_t currentVersion;
+    uint32_t targetVersion;
+
+    OTARequestorInterface * requestor = chip::GetRequestorInstance();
+    ReturnErrorCodeIf(requestor == nullptr, CHIP_ERROR_INTERNAL);
+
+    targetVersion = requestor->GetTargetVersion();
+    ReturnErrorOnFailure(DeviceLayer::ConfigurationMgr().GetSoftwareVersion(currentVersion));
+    if (currentVersion != targetVersion)
+    {
+        ChipLogError(SoftwareUpdate, "Current sw version %lu is different than the expected sw version = %lu", currentVersion,
+                     targetVersion);
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::SetBlock(ByteSpan & block)
+{
+    if (block.empty())
+    {
+        return CHIP_NO_ERROR;
+    }
+
+    if (mBlock.size() < block.size())
+    {
+        if (!mBlock.empty())
+        {
+            ReleaseBlock();
+        }
+        uint8_t * mBlock_ptr = static_cast<uint8_t *>(chip::Platform::MemoryAlloc(block.size()));
+        if (mBlock_ptr == nullptr)
+        {
+            return CHIP_ERROR_NO_MEMORY;
+        }
+        mBlock = MutableByteSpan(mBlock_ptr, block.size());
+    }
+
+    CHIP_ERROR err = CopySpanToMutableSpan(block, mBlock);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(SoftwareUpdate, "Cannot copy block data: %" CHIP_ERROR_FORMAT, err.Format());
+        return err;
+    }
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::HandleFinalize(intptr_t context)
+{
+    ChipLogError(SoftwareUpdate, "HandleFinalize begin");
+    CHIP_ERROR error      = CHIP_NO_ERROR;
+    auto * imageProcessor = reinterpret_cast<OTAMultiImageProcessorImpl *>(context);
+    if (imageProcessor == nullptr)
+    {
+        return;
+    }
+
+    error = imageProcessor->ProcessFinalize();
+
+    imageProcessor->mParams.downloadedBytes += imageProcessor->mBlock.size();
+
+    imageProcessor->ReleaseBlock();
+
+    if (error != CHIP_NO_ERROR)
+    {
+        ChipLogError(SoftwareUpdate, "ProcessFinalize() error");
+        imageProcessor->mDownloader->EndDownload(CHIP_ERROR_WRITE_FAILED);
+        return;
+    }
+
+    ChipLogProgress(SoftwareUpdate, "OTA image downloaded successfully");
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ProcessFinalize()
+{
+    for (auto const & pair : this->mProcessorMap)
+    {
+        pair.second->FinalizeAction();
+    }
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::HandleApply(intptr_t context)
+{
+    CHIP_ERROR error      = CHIP_NO_ERROR;
+    auto * imageProcessor = reinterpret_cast<OTAMultiImageProcessorImpl *>(context);
+
+    ChipLogProgress(SoftwareUpdate, "HandleApply: started");
+
+    // Force KVS to store pending keys such as data from StoreCurrentUpdateInfo()
+    chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl().ForceKeyMapSave();
+
+    if (imageProcessor == nullptr)
+    {
+        return;
+    }
+
+    for (auto const & pair : imageProcessor->mProcessorMap)
+    {
+        if (pair.second->WasSelected())
+        {
+            error = pair.second->ApplyAction();
+            if (error != CHIP_NO_ERROR)
+            {
+                ChipLogError(SoftwareUpdate, "Apply action for tag %d processor failed.", (uint8_t) pair.first);
+                // Revert all previously applied actions if current apply action fails.
+                // Reset image processor and requestor states.
+                imageProcessor->AbortAllProcessors();
+                imageProcessor->Clear();
+                GetRequestorInstance()->Reset();
+
+                return;
+            }
+        }
+    }
+
+    for (auto const & pair : imageProcessor->mProcessorMap)
+    {
+        pair.second->Clear();
+        pair.second->SetWasSelected(false);
+    }
+
+    imageProcessor->mAccumulator.Clear();
+
+    ChipLogProgress(SoftwareUpdate, "HandleApply: Finished");
+
+    // TODO: check where to put this
+    // ConfigurationManagerImpl().StoreSoftwareUpdateCompleted();
+
+    // This reboots the device
+    CORE_CRITICAL_SECTION(bootloader_rebootAndInstall();)
+}
+
+CHIP_ERROR OTAMultiImageProcessorImpl::ReleaseBlock()
+{
+    if (mBlock.data() != nullptr)
+    {
+        chip::Platform::MemoryFree(mBlock.data());
+    }
+
+    mBlock = MutableByteSpan();
+    return CHIP_NO_ERROR;
+}
+
+void OTAMultiImageProcessorImpl::FetchNextData(uint32_t context)
+{
+    auto * imageProcessor = &OTAMultiImageProcessorImpl::GetDefaultInstance();
+    SystemLayer().ScheduleLambda([imageProcessor] {
+        if (imageProcessor->mDownloader)
+        {
+            imageProcessor->mDownloader->FetchNextData();
+        }
+    });
+}
+
+OTAMultiImageProcessorImpl & OTAMultiImageProcessorImpl::GetDefaultInstance()
+{
+    return gImageProcessor;
+}
+
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h
new file mode 100644
index 0000000..3e9cf0d
--- /dev/null
+++ b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h
@@ -0,0 +1,99 @@
+/*
+ *
+ *    Copyright (c) 2021-2023 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.
+ */
+
+#pragma once
+
+#include <app/clusters/ota-requestor/OTADownloader.h>
+#include <app/clusters/ota-requestor/OTARequestorInterface.h>
+#include <include/platform/CHIPDeviceLayer.h>
+#include <include/platform/OTAImageProcessor.h>
+#include <lib/core/OTAImageHeader.h>
+#include <map>
+#include <platform/silabs/multi-ota/OTATlvProcessor.h>
+
+/*
+ * This hook is called at the end of OTAMultiImageProcessorImpl::Init.
+ * It should generally register the OTATlvProcessor instances.
+ */
+
+namespace chip {
+
+class OTAMultiImageProcessorImpl : public OTAImageProcessorInterface
+{
+public:
+    using ProviderLocation = chip::OTARequestorInterface::ProviderLocationType;
+
+    CHIP_ERROR Init(OTADownloader * downloader);
+    CHIP_ERROR OtaHookInit();
+    static CHIP_ERROR ProcessDescriptor(void * descriptor);
+    void Clear();
+
+    //////////// OTAImageProcessorInterface Implementation ///////////////
+    CHIP_ERROR PrepareDownload() override;
+    CHIP_ERROR Finalize() override;
+    CHIP_ERROR Apply() override;
+    CHIP_ERROR Abort() override;
+    CHIP_ERROR ProcessBlock(ByteSpan & block) override;
+    bool IsFirstImageRun() override;
+    CHIP_ERROR ConfirmCurrentImage() override;
+
+    void SetOTADownloader(OTADownloader * downloader) { mDownloader = downloader; }
+
+    CHIP_ERROR ProcessHeader(ByteSpan & block);
+    CHIP_ERROR ProcessPayload(ByteSpan & block);
+    CHIP_ERROR ProcessFinalize();
+    CHIP_ERROR SelectProcessor(ByteSpan & block);
+    CHIP_ERROR RegisterProcessor(uint32_t tag, OTATlvProcessor * processor);
+
+    static void FetchNextData(uint32_t context);
+    static OTAMultiImageProcessorImpl & GetDefaultInstance();
+
+private:
+    //////////// Actual handlers for the OTAImageProcessorInterface ///////////////
+    static void HandlePrepareDownload(intptr_t context);
+    static void HandleFinalize(intptr_t context);
+    static void HandleApply(intptr_t context);
+    static void HandleAbort(intptr_t context);
+    static void HandleProcessBlock(intptr_t context);
+
+    void HandleStatus(CHIP_ERROR status);
+
+    /**
+     * Called to allocate memory for mBlock if necessary and set it to block
+     */
+    CHIP_ERROR SetBlock(ByteSpan & block);
+
+    /**
+     * Called to release allocated memory for mBlock
+     */
+    CHIP_ERROR ReleaseBlock();
+
+    /**
+     * Call AbortAction for all processors that were used
+     */
+    void AbortAllProcessors();
+
+    MutableByteSpan mBlock;
+    OTADownloader * mDownloader;
+    OTAImageHeaderParser mHeaderParser;
+    OTATlvProcessor * mCurrentProcessor = nullptr;
+    OTADataAccumulator mAccumulator;
+    std::map<uint32_t, OTATlvProcessor *> mProcessorMap;
+};
+
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/OTATlvProcessor.cpp b/src/platform/silabs/multi-ota/OTATlvProcessor.cpp
new file mode 100644
index 0000000..a5da7ea
--- /dev/null
+++ b/src/platform/silabs/multi-ota/OTATlvProcessor.cpp
@@ -0,0 +1,171 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+
+#include <lib/core/TLV.h>
+#include <lib/support/BufferReader.h>
+#include <platform/internal/CHIPDeviceLayerInternal.h>
+
+#include <platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h>
+#include <platform/silabs/multi-ota/OTATlvProcessor.h>
+#if OTA_ENCRYPTION_ENABLE
+#include "OtaUtils.h"
+#include "rom_aes.h"
+#endif
+namespace chip {
+
+#if OTA_ENCRYPTION_ENABLE
+constexpr uint8_t au8Iv[] = { 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00 };
+#endif
+CHIP_ERROR OTATlvProcessor::Process(ByteSpan & block)
+{
+    CHIP_ERROR status     = CHIP_NO_ERROR;
+    uint32_t bytes        = chip::min(mLength - mProcessedLength, static_cast<uint32_t>(block.size()));
+    ByteSpan relevantData = block.SubSpan(0, bytes);
+
+    status = ProcessInternal(relevantData);
+    if (!IsError(status))
+    {
+        mProcessedLength += bytes;
+        block = block.SubSpan(bytes);
+        if (mProcessedLength == mLength)
+        {
+            status = ExitAction();
+            if (!IsError(status))
+            {
+                // If current block was processed fully and the block still contains data, it
+                // means that the block contains another TLV's data and the current processor
+                // should be changed by OTAMultiImageProcessorImpl.
+                return CHIP_OTA_CHANGE_PROCESSOR;
+            }
+        }
+    }
+
+    return status;
+}
+
+void OTATlvProcessor::ClearInternal()
+{
+    mLength          = 0;
+    mProcessedLength = 0;
+    mWasSelected     = false;
+#if OTA_ENCRYPTION_ENABLE
+    mIVOffset = 0;
+#endif
+}
+
+bool OTATlvProcessor::IsError(CHIP_ERROR & status)
+{
+    return status != CHIP_NO_ERROR && status != CHIP_ERROR_BUFFER_TOO_SMALL && status != CHIP_OTA_FETCH_ALREADY_SCHEDULED;
+}
+
+void OTADataAccumulator::Init(uint32_t threshold)
+{
+    mThreshold    = threshold;
+    mBufferOffset = 0;
+    mBuffer.Alloc(mThreshold);
+}
+
+void OTADataAccumulator::Clear()
+{
+    mThreshold    = 0;
+    mBufferOffset = 0;
+    mBuffer.Free();
+}
+
+CHIP_ERROR OTADataAccumulator::Accumulate(ByteSpan & block)
+{
+    uint32_t numBytes = chip::min(mThreshold - mBufferOffset, static_cast<uint32_t>(block.size()));
+    memcpy(&mBuffer[mBufferOffset], block.data(), numBytes);
+    mBufferOffset += numBytes;
+    block = block.SubSpan(numBytes);
+
+    if (mBufferOffset < mThreshold)
+    {
+        return CHIP_ERROR_BUFFER_TOO_SMALL;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+#if OTA_ENCRYPTION_ENABLE
+CHIP_ERROR OTATlvProcessor::vOtaProcessInternalEncryption(MutableByteSpan & block)
+{
+    uint8_t iv[16];
+    uint8_t key[kOTAEncryptionKeyLength];
+    uint8_t dataOut[16] = { 0 };
+    uint32_t u32IVCount;
+    uint32_t Offset = 0;
+    uint8_t data;
+    tsReg128 sKey;
+    aesContext_t Context;
+
+    memcpy(iv, au8Iv, sizeof(au8Iv));
+
+    u32IVCount = (((uint32_t) iv[12]) << 24) | (((uint32_t) iv[13]) << 16) | (((uint32_t) iv[14]) << 8) | (iv[15]);
+    u32IVCount += (mIVOffset >> 4);
+
+    iv[12] = (uint8_t) ((u32IVCount >> 24) & 0xff);
+    iv[13] = (uint8_t) ((u32IVCount >> 16) & 0xff);
+    iv[14] = (uint8_t) ((u32IVCount >> 8) & 0xff);
+    iv[15] = (uint8_t) (u32IVCount & 0xff);
+
+    if (Encoding::HexToBytes(OTA_ENCRYPTION_KEY, strlen(OTA_ENCRYPTION_KEY), key, kOTAEncryptionKeyLength) !=
+        kOTAEncryptionKeyLength)
+    {
+        // Failed to convert the OTAEncryptionKey string to octstr type value
+        return CHIP_ERROR_INVALID_STRING_LENGTH;
+    }
+
+    ByteSpan KEY = ByteSpan(key);
+    Encoding::LittleEndian::Reader reader_key(KEY.data(), KEY.size());
+    ReturnErrorOnFailure(reader_key.Read32(&sKey.u32register0)
+                             .Read32(&sKey.u32register1)
+                             .Read32(&sKey.u32register2)
+                             .Read32(&sKey.u32register3)
+                             .StatusCode());
+
+    while (Offset + 16 <= block.size())
+    {
+        /*Encrypt the IV*/
+        Context.mode         = AES_MODE_ECB_ENCRYPT;
+        Context.pSoftwareKey = (uint32_t *) &sKey;
+        AES_128_ProcessBlocks(&Context, (uint32_t *) &iv[0], (uint32_t *) &dataOut[0], 1);
+
+        /* Decrypt a block of the buffer */
+        for (uint8_t i = 0; i < 16; i++)
+        {
+            data = block[Offset + i] ^ dataOut[i];
+            memcpy(&block[Offset + i], &data, sizeof(uint8_t));
+        }
+
+        /* increment the IV for the next block  */
+        u32IVCount++;
+
+        iv[12] = (uint8_t) ((u32IVCount >> 24) & 0xff);
+        iv[13] = (uint8_t) ((u32IVCount >> 16) & 0xff);
+        iv[14] = (uint8_t) ((u32IVCount >> 8) & 0xff);
+        iv[15] = (uint8_t) (u32IVCount & 0xff);
+
+        Offset += 16; /* increment the buffer offset */
+        mIVOffset += 16;
+    }
+
+    return CHIP_NO_ERROR;
+}
+#endif
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/OTATlvProcessor.h b/src/platform/silabs/multi-ota/OTATlvProcessor.h
new file mode 100644
index 0000000..9e8e56a
--- /dev/null
+++ b/src/platform/silabs/multi-ota/OTATlvProcessor.h
@@ -0,0 +1,159 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+
+#pragma once
+
+#include <lib/core/Optional.h>
+#include <lib/support/ScopedBuffer.h>
+#include <lib/support/Span.h>
+
+namespace chip {
+
+#define CHIP_ERROR_TLV_PROCESSOR(e)                                                                                                \
+    ChipError(ChipError::Range::kLastRange, ((uint8_t) ChipError::Range::kLastRange << 3) | e, __FILE__, __LINE__)
+
+#define CHIP_OTA_TLV_CONTINUE_PROCESSING CHIP_ERROR_TLV_PROCESSOR(0x01)
+#define CHIP_OTA_CHANGE_PROCESSOR CHIP_ERROR_TLV_PROCESSOR(0x02)
+#define CHIP_OTA_PROCESSOR_NOT_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x03)
+#define CHIP_OTA_PROCESSOR_ALREADY_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x04)
+#define CHIP_OTA_PROCESSOR_CLIENT_INIT CHIP_ERROR_TLV_PROCESSOR(0x05)
+#define CHIP_OTA_PROCESSOR_MAKE_ROOM CHIP_ERROR_TLV_PROCESSOR(0x06)
+#define CHIP_OTA_PROCESSOR_PUSH_CHUNK CHIP_ERROR_TLV_PROCESSOR(0x07)
+#define CHIP_OTA_PROCESSOR_IMG_AUTH CHIP_ERROR_TLV_PROCESSOR(0x08)
+#define CHIP_OTA_FETCH_ALREADY_SCHEDULED CHIP_ERROR_TLV_PROCESSOR(0x09)
+#define CHIP_OTA_PROCESSOR_IMG_COMMIT CHIP_ERROR_TLV_PROCESSOR(0x0A)
+#define CHIP_OTA_PROCESSOR_CB_NOT_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x0B)
+#define CHIP_OTA_PROCESSOR_EEPROM_OFFSET CHIP_ERROR_TLV_PROCESSOR(0x0C)
+#define CHIP_OTA_PROCESSOR_EXTERNAL_STORAGE CHIP_ERROR_TLV_PROCESSOR(0x0D)
+#define CHIP_OTA_PROCESSOR_START_IMAGE CHIP_ERROR_TLV_PROCESSOR(0x0E)
+#define SL_GENERIC_OTA_ERROR CHIP_ERROR_TLV_PROCESSOR(0x0E)
+
+// Descriptor constants
+inline constexpr size_t kVersionStringSize = 64;
+inline constexpr size_t kBuildDateSize     = 64;
+
+/**
+ * Used alongside RegisterDescriptorCallback to register
+ * a custom descriptor processing function with a certain
+ * TLV processor.
+ */
+typedef CHIP_ERROR (*ProcessDescriptor)(void * descriptor);
+
+struct OTATlvHeader
+{
+    uint32_t tag;
+    uint32_t length;
+};
+
+/**
+ * This class defines an interface for a Matter TLV processor.
+ * Instances of derived classes can be registered as processors
+ * in OTAMultiImageProcessorImpl. Based on the TLV type, a certain
+ * processor is used to process subsequent blocks until the number
+ * of bytes found in the metadata is processed. In case a block contains
+ * data from two different TLVs, the processor should ensure the remaining
+ * data is returned in the block passed as input.
+ * The default processors: application, SSBL and factory data are registered
+ * in OTAMultiImageProcessorImpl::Init through OtaHookInit.
+ * Applications should use OTAMultiImageProcessorImpl::RegisterProcessor
+ * to register additional processors.
+ */
+class OTATlvProcessor
+{
+public:
+    virtual ~OTATlvProcessor() {}
+
+    virtual CHIP_ERROR Init()           = 0;
+    virtual CHIP_ERROR Clear()          = 0;
+    virtual CHIP_ERROR ApplyAction()    = 0;
+    virtual CHIP_ERROR FinalizeAction() = 0;
+    virtual CHIP_ERROR ExitAction() { return CHIP_NO_ERROR; }
+
+    CHIP_ERROR Process(ByteSpan & block);
+    void RegisterDescriptorCallback(ProcessDescriptor callback) { mCallbackProcessDescriptor = callback; }
+    void SetLength(uint32_t length) { mLength = length; }
+    void SetWasSelected(bool selected) { mWasSelected = selected; }
+    bool WasSelected() { return mWasSelected; }
+#if OTA_ENCRYPTION_ENABLE
+    CHIP_ERROR vOtaProcessInternalEncryption(MutableByteSpan & block);
+#endif
+
+protected:
+    /**
+     * @brief Process custom TLV payload
+     *
+     * The method takes subsequent chunks of the Matter OTA image file and processes them.
+     * If more image chunks are needed, CHIP_ERROR_BUFFER_TOO_SMALL error is returned.
+     * Other error codes indicate that an error occurred during processing. Fetching
+     * next data is scheduled automatically by OTAMultiImageProcessorImpl if the return value
+     * is neither an error code, nor CHIP_OTA_FETCH_ALREADY_SCHEDULED (which implies the
+     * scheduling is done inside ProcessInternal or will be done in the future, through a
+     * callback).
+     *
+     * @param block 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 current TLV processor.
+     *
+     * @retval CHIP_NO_ERROR                    Block was processed successfully.
+     * @retval CHIP_ERROR_BUFFER_TOO_SMALL      Provided buffers are insufficient to decode some
+     *                                          metadata (e.g. a descriptor).
+     * @retval CHIP_OTA_FETCH_ALREADY_SCHEDULED Should be returned if ProcessInternal schedules
+     *                                          fetching next data (e.g. through a callback).
+     * @retval Error code                       Something went wrong. Current OTA process will be
+     *                                          canceled.
+     */
+    virtual CHIP_ERROR ProcessInternal(ByteSpan & block) = 0;
+
+    void ClearInternal();
+
+    bool IsError(CHIP_ERROR & status);
+
+#if OTA_ENCRYPTION_ENABLE
+    /*ota decryption*/
+    uint32_t mIVOffset = 0;
+    /* Expected byte size of the OTAEncryptionKeyLength */
+    static constexpr size_t kOTAEncryptionKeyLength = 16;
+#endif
+    uint32_t mLength                             = 0;
+    uint32_t mProcessedLength                    = 0;
+    bool mWasSelected                            = false;
+    ProcessDescriptor mCallbackProcessDescriptor = nullptr;
+};
+
+/**
+ * This class can be used to accumulate data until a given threshold.
+ * Should be used by OTATlvProcessor derived classes if they need
+ * metadata accumulation (e.g. for custom header decoding).
+ */
+class OTADataAccumulator
+{
+public:
+    void Init(uint32_t threshold);
+    void Clear();
+    CHIP_ERROR Accumulate(ByteSpan & block);
+
+    inline uint8_t * data() { return mBuffer.Get(); }
+    inline uint32_t GetThreshold() { return mThreshold; }
+
+private:
+    uint32_t mThreshold;
+    uint32_t mBufferOffset;
+    Platform::ScopedMemoryBuffer<uint8_t> mBuffer;
+};
+
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp
new file mode 100644
index 0000000..fdf4c3d
--- /dev/null
+++ b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp
@@ -0,0 +1,224 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+
+#include <platform/internal/CHIPDeviceLayerInternal.h>
+#include <platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h>
+#include <platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h>
+
+#include <app/clusters/ota-requestor/OTARequestorInterface.h>
+
+extern "C" {
+#include "btl_interface.h"
+#include "em_bus.h" // For CORE_CRITICAL_SECTION
+#if SL_WIFI
+#include "spi_multiplex.h"
+#endif // SL_WIFI
+}
+
+/// No error, operation OK
+#define SL_BOOTLOADER_OK 0L
+// TODO: more descriptive error codes
+#define SL_OTA_ERROR 1L
+
+namespace chip {
+
+// Define static memebers
+uint8_t OTAFirmwareProcessor::mSlotId                                                  = 0;
+uint32_t OTAFirmwareProcessor::mWriteOffset                                            = 0;
+uint16_t OTAFirmwareProcessor::writeBufOffset                                          = 0;
+uint8_t OTAFirmwareProcessor::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 };
+
+CHIP_ERROR OTAFirmwareProcessor::Init()
+{
+    ReturnErrorCodeIf(mCallbackProcessDescriptor == nullptr, CHIP_OTA_PROCESSOR_CB_NOT_REGISTERED);
+    mAccumulator.Init(sizeof(Descriptor));
+#if OTA_ENCRYPTION_ENABLE
+    mUnalignmentNum = 0;
+#endif
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAFirmwareProcessor::Clear()
+{
+    OTATlvProcessor::ClearInternal();
+    mAccumulator.Clear();
+    mDescriptorProcessed = false;
+#if OTA_ENCRYPTION_ENABLE
+    mUnalignmentNum = 0;
+#endif
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAFirmwareProcessor::ProcessInternal(ByteSpan & block)
+{
+    uint32_t err = SL_BOOTLOADER_OK;
+    if (!mDescriptorProcessed)
+    {
+        ReturnErrorOnFailure(ProcessDescriptor(block));
+    }
+
+    uint32_t blockReadOffset = 0;
+    while (blockReadOffset < block.size())
+    {
+        writeBuffer[writeBufOffset] = *((block.data()) + blockReadOffset);
+        writeBufOffset++;
+        blockReadOffset++;
+        if (writeBufOffset == kAlignmentBytes)
+        {
+            writeBufOffset = 0;
+#if SL_BTLCTRL_MUX
+            err = sl_wfx_host_pre_bootloader_spi_transfer();
+            if (err != SL_STATUS_OK)
+            {
+                ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err);
+                return;
+            }
+#endif // SL_BTLCTRL_MUX
+            CORE_CRITICAL_SECTION(err = bootloader_eraseWriteStorage(mSlotId, mWriteOffset, writeBuffer, kAlignmentBytes);)
+#if SL_BTLCTRL_MUX
+            err = sl_wfx_host_post_bootloader_spi_transfer();
+            if (err != SL_STATUS_OK)
+            {
+                ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err);
+                return;
+            }
+#endif // SL_BTLCTRL_MUX
+            if (err)
+            {
+                ChipLogError(SoftwareUpdate, "bootloader_eraseWriteStorage() error: %ld", err);
+                // TODO: add this somewhere
+                // imageProcessor->mDownloader->EndDownload(CHIP_ERROR_WRITE_FAILED);
+                // TODO: Replace CHIP_ERROR_CANCELLED with new error statement
+                return CHIP_ERROR_CANCELLED;
+            }
+            mWriteOffset += kAlignmentBytes;
+        }
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAFirmwareProcessor::ProcessDescriptor(ByteSpan & block)
+{
+    ReturnErrorOnFailure(mAccumulator.Accumulate(block));
+    ReturnErrorOnFailure(mCallbackProcessDescriptor(static_cast<void *>(mAccumulator.data())));
+
+    mDescriptorProcessed = true;
+    mAccumulator.Clear();
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAFirmwareProcessor::ApplyAction()
+{
+    uint32_t err = SL_BOOTLOADER_OK;
+
+#if SL_BTLCTRL_MUX
+    err = sl_wfx_host_pre_bootloader_spi_transfer();
+    if (err != SL_STATUS_OK)
+    {
+        ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err);
+        return SL_GENERIC_OTA_ERROR;
+    }
+#endif // SL_BTLCTRL_MUX
+    CORE_CRITICAL_SECTION(err = bootloader_verifyImage(mSlotId, NULL);)
+    if (err != SL_BOOTLOADER_OK)
+    {
+        ChipLogError(SoftwareUpdate, "bootloader_verifyImage() error: %ld", err);
+        // Call the OTARequestor API to reset the state
+        GetRequestorInstance()->CancelImageUpdate();
+#if SL_BTLCTRL_MUX
+        err = sl_wfx_host_post_bootloader_spi_transfer();
+        if (err != SL_STATUS_OK)
+        {
+            ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err);
+            return SL_GENERIC_OTA_ERROR;
+        }
+#endif // SL_BTLCTRL_MUX
+        return SL_GENERIC_OTA_ERROR;
+    }
+
+    CORE_CRITICAL_SECTION(err = bootloader_setImageToBootload(mSlotId);)
+    if (err != SL_BOOTLOADER_OK)
+    {
+        ChipLogError(SoftwareUpdate, "bootloader_setImageToBootload() error: %ld", err);
+        // Call the OTARequestor API to reset the state
+        GetRequestorInstance()->CancelImageUpdate();
+#if SL_BTLCTRL_MUX
+        err = sl_wfx_host_post_bootloader_spi_transfer();
+        if (err != SL_STATUS_OK)
+        {
+            ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err);
+            return SL_GENERIC_OTA_ERROR;
+        }
+#endif // SL_BTLCTRL_MUX
+        return SL_GENERIC_OTA_ERROR;
+    }
+
+#if SL_BTLCTRL_MUX
+    err = sl_wfx_host_post_bootloader_spi_transfer();
+    if (err != SL_STATUS_OK)
+    {
+        ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err);
+        return SL_GENERIC_OTA_ERROR;
+    }
+#endif // SL_BTLCTRL_MUX
+    // This reboots the device
+    // CORE_CRITICAL_SECTION(bootloader_rebootAndInstall();)
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR OTAFirmwareProcessor::FinalizeAction()
+{
+    uint32_t err = SL_BOOTLOADER_OK;
+
+    // Pad the remainder of the write buffer with zeros and write it to bootloader storage
+    if (writeBufOffset != 0)
+    {
+
+        while (writeBufOffset != kAlignmentBytes)
+        {
+            writeBuffer[writeBufOffset] = 0;
+            writeBufOffset++;
+        }
+#if SL_BTLCTRL_MUX
+        err = sl_wfx_host_pre_bootloader_spi_transfer();
+        if (err != SL_STATUS_OK)
+        {
+            ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err);
+            return SL_GENERIC_OTA_ERROR;
+        }
+#endif // SL_BTLCTRL_MUX
+        CORE_CRITICAL_SECTION(err = bootloader_eraseWriteStorage(mSlotId, mWriteOffset, writeBuffer, kAlignmentBytes);)
+#if SL_BTLCTRL_MUX
+        err = sl_wfx_host_post_bootloader_spi_transfer();
+        if (err != SL_STATUS_OK)
+        {
+            ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err);
+            return SL_GENERIC_OTA_ERROR;
+        }
+#endif // SL_BTLCTRL_MUX
+    }
+
+    return err ? CHIP_ERROR_WRITE_FAILED : CHIP_NO_ERROR;
+}
+
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h
new file mode 100644
index 0000000..c383e74
--- /dev/null
+++ b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h
@@ -0,0 +1,59 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+
+#pragma once
+
+#include <lib/support/Span.h>
+#include <platform/silabs/multi-ota/OTATlvProcessor.h>
+
+namespace chip {
+
+class OTAFirmwareProcessor : public OTATlvProcessor
+{
+public:
+    struct Descriptor
+    {
+        uint32_t version;
+        char versionString[kVersionStringSize];
+        char buildDate[kBuildDateSize];
+    };
+
+    CHIP_ERROR Init() override;
+    CHIP_ERROR Clear() override;
+    CHIP_ERROR ApplyAction() override;
+    CHIP_ERROR FinalizeAction() override;
+
+private:
+    CHIP_ERROR ProcessInternal(ByteSpan & block) override;
+    CHIP_ERROR ProcessDescriptor(ByteSpan & block);
+
+    OTADataAccumulator mAccumulator;
+    bool mDescriptorProcessed = false;
+#if OTA_ENCRYPTION_ENABLE
+    uint32_t mUnalignmentNum;
+#endif
+    static constexpr size_t kAlignmentBytes = 64;
+    static uint32_t mWriteOffset; // End of last written block
+    static uint8_t mSlotId;       // Bootloader storage slot
+    // Bootloader storage API requires the buffer size to be a multiple of 4.
+    static uint8_t writeBuffer[kAlignmentBytes] __attribute__((aligned(4)));
+    // Offset indicates how far the write buffer has been filled
+    static uint16_t writeBufOffset;
+};
+
+} // namespace chip
diff --git a/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp b/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp
new file mode 100644
index 0000000..cddb669
--- /dev/null
+++ b/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp
@@ -0,0 +1,44 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+
+#include <include/platform/CHIPDeviceLayer.h>
+#include <platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h>
+
+#include <app/clusters/ota-requestor/OTARequestorInterface.h>
+
+#include <platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h>
+
+CHIP_ERROR chip::OTAMultiImageProcessorImpl::ProcessDescriptor(void * descriptor)
+{
+    auto desc = static_cast<chip::OTAFirmwareProcessor::Descriptor *>(descriptor);
+    ChipLogDetail(SoftwareUpdate, "Descriptor: %ld, %s, %s", desc->version, desc->versionString, desc->buildDate);
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR chip::OTAMultiImageProcessorImpl::OtaHookInit()
+{
+    static chip::OTAFirmwareProcessor sApplicationProcessor;
+
+    sApplicationProcessor.RegisterDescriptorCallback(ProcessDescriptor);
+
+    auto & imageProcessor = chip::OTAMultiImageProcessorImpl::GetDefaultInstance();
+    ReturnErrorOnFailure(imageProcessor.RegisterProcessor(1, &sApplicationProcessor));
+
+    return CHIP_NO_ERROR;
+}
diff --git a/third_party/silabs/efr32_sdk.gni b/third_party/silabs/efr32_sdk.gni
index 55934ae..9a2a1ec 100644
--- a/third_party/silabs/efr32_sdk.gni
+++ b/third_party/silabs/efr32_sdk.gni
@@ -77,6 +77,9 @@
 
   # Use SLC generated files
   slc_reuse_files = false
+
+  # Multi-chip OTA
+  chip_enable_multi_ota_requestor = false
 }
 examples_plat_dir = "${chip_root}/examples/platform/silabs/efr32"
 silabs_plat_efr32_wifi_dir = "${chip_root}/src/platform/silabs/efr32/wifi"