Skip to content

Latest commit

 

History

History
388 lines (309 loc) · 13.8 KB

kboot_classes_example.md

File metadata and controls

388 lines (309 loc) · 13.8 KB

Interactively running the kboot_classes.py file

...from inside the k210 device console

minicom -D /dev/ttyUSB1 -b 115200

The goal of these classes are to model Kboot sectors (both bootloader stages, configurations, and firmware) in SPI Flash. These are intended to replace other function-based tools in this repo, to be more expressive, and to be easier to work with.

We'll connect to the k210 device console over usb, interrupt krux with <ctrl>-c, then we'll get ready to copy/paste.

We need to stop the watchdog timer and import utils from the Maix package.

from machine import WDT
WDT().stop()
from Maix import utils

We need to copy/paste the contents of kboot_classes.py into the k210 console.

copy/paste this python code
'''
classes to model Kboot bootloader, configuration, and application sectors

assumes that utils.flash_read() behaves as if imported from Maix:
ie: `from Maix import utils`
'''


from binascii import crc32
from hashlib import sha256


class KbootConstants:
    STAGE0_ADDRESS = 0x0
    STAGE1_ADDRESS = 0x1000
    MAIN_CONFIG_ADDRESS = 0x4000
    BACKUP_CONFIG_ADDRESS = 0x5000
    BASE_CONFIG_ENTRY_ID = 0x5aa5d0c0
    APP_ADDRESS_RANGE = (0x10000, 0x800000)
    APP_SIZE_RANGE = (0x4000, 0x300000)


class KbootConfigEntry:
    def __init__(self,
        app_address,
        is_active=True, 
        ck_crc32=False,
        ck_sha256=False,
        ck_size=False,
        app_size=KbootConstants.APP_SIZE_RANGE[0],
        app_crc32=0,
        app_name='firmware'
    ):
        if KbootConstants.APP_ADDRESS_RANGE[0] <= app_address <= KbootConstants.APP_ADDRESS_RANGE[1]:
            self.app_address = app_address
        else:
            raise ValueError('Valid app address range is {} to {}'.format(
                *[hex(x) for x in KbootConstants.APP_ADDRESS_RANGE]
            ))

        if KbootConstants.APP_SIZE_RANGE[0] <= app_size <= KbootConstants.APP_SIZE_RANGE[1]:
            self.app_size = app_size
        else:
            raise ValueError('Valid app size range is {} to {} bytes'.format(
                *KbootConstants.APP_SIZE_RANGE
            )) 

        self.is_active = bool(is_active)
        self.ck_crc32 = bool(ck_crc32)
        self.ck_sha256 = bool(ck_sha256)
        self.ck_size = bool(ck_size)
        self.app_crc32 = int(app_crc32)
        self.app_name = str(app_name)

    @classmethod
    def from_bytes(cls, raw_bytes):
        if type(raw_bytes) != bytes or len(raw_bytes) != 32:
            raise ValueError('raw_bytes must be of type bytes and of length 32')

        id_flags = int.from_bytes(raw_bytes[:4], 'big')
        if KbootConstants.BASE_CONFIG_ENTRY_ID <= id_flags <= KbootConstants.BASE_CONFIG_ENTRY_ID+16:
            is_active = bool(id_flags & 1)
            ck_crc32 = bool(id_flags & 2)
            ck_sha256 = bool(id_flags & 4)
            ck_size = bool(id_flags & 8)
        else:
            raise ValueError('First 28 bits of Entry ID must be {}'.format(
                hex(KbootConstants.BASE_CONFIG_ENTRY_ID)[:-1]))

        app_address = int.from_bytes(raw_bytes[4:8], 'big')
        app_size = int.from_bytes(raw_bytes[8:12], 'big')
        app_crc32 = int.from_bytes(raw_bytes[12:16], 'big')
        app_name = raw_bytes[16:].decode('utf8')

        return cls(
            app_address=app_address,
            is_active=is_active,
            ck_crc32=ck_crc32,
            ck_sha256=ck_sha256,
            ck_size=ck_size,
            app_size=app_size,
            app_crc32=app_crc32,
            app_name=app_name
        )

    def serialize(self):
        id_entry = KbootConstants.BASE_CONFIG_ENTRY_ID
        if self.is_active: id_entry += 1
        if self.ck_crc32: id_entry += 2
        if self.ck_sha256: id_entry += 4
        if self.ck_size: id_entry += 8
        raw_bytes = id_entry.to_bytes(4, 'big')

        raw_bytes += self.app_address.to_bytes(4, 'big')
        raw_bytes += self.app_size.to_bytes(4, 'big')
        raw_bytes += self.app_crc32.to_bytes(4, 'big')
        raw_bytes += (self.app_name.encode('utf8') + b'\x00'*16)[:16]

        return raw_bytes

    def __str__(self):
        return '{}: flags: {}/{}/{}/{}, address: {}, size: {}'.format(
            self.app_name,
            'active' if self.is_active else 'NOT-active',
            'ck_crc32' if self.ck_crc32 else 'NO-ck_crc32',
            'ck_sha256' if self.ck_sha256 else 'NO-ck_sha256',
            'ck_size' if self.ck_size else 'NO-ck_size',
            hex(self.app_address),
            self.app_size
        )


class KbootConfigSector:
    def __init__(self,
        entries=[],
        config_flags=0,
        reserved=0,
        user_data=0
    ):
        if 0 <= len(entries) <= 8 and set([type(x)==KbootConfigEntry for x in entries]) == set([True]):
            self.entries = entries
        else:
            raise ValueError('entries must be of type KbootConfigEntry and of length 0-8')
        
        self.config_flags = config_flags
        self.reserved = reserved
        self.user_data = user_data

    @classmethod
    def from_bytes(cls, raw_bytes):
        if type(raw_bytes) != bytes or len(raw_bytes) != 4096:
            raise ValueError('raw_bytes must be of type bytes and of length 4096')

        entries = []
        for i in range(0, 32*8, 32):
            try: entries.append(KbootConfigEntry.from_bytes(raw_bytes[i:i+32]))
            except: pass

        config_flags = int.from_bytes(raw_bytes[256:260], 'big')
        reserved = int.from_bytes(raw_bytes[260:264], 'big')

        if raw_bytes[264:288] != b'\x00'*24:
            raise ValueError('24 bytes undocumented between reserved and user_data should be null')

        user_data = int.from_bytes(raw_bytes[288:292], 'big')

        if raw_bytes[292:] != b'\x00'*3804:
            raise ValueError('3804 bytes to pad end of sector should be null')

        return cls(
            entries=entries,
            config_flags=config_flags,
            reserved=reserved,
            user_data=user_data
        )

    def serialize(self):
        raw_bytes = b''
        for i in range(8):
            if len(self.entries) > i:
                raw_bytes += self.entries[i].serialize()
            else:
                raw_bytes += b'\x00' * 32

        raw_bytes += self.config_flags.to_bytes(4, 'big')
        raw_bytes += self.reserved.to_bytes(4, 'big')
        raw_bytes += b'\x00' * 24
        raw_bytes += self.user_data.to_bytes(4, 'big')
        raw_bytes += b'\x00' * 3804

        return raw_bytes

    def sha256(self):
        return sha256(self.serialize()).digest()

    def __str__(self):
        return 'config_flags: {}, user_data: {}, entries:\n  {}'.format(
            hexlify(self.config_flags.to_bytes(4, 'big')).decode(),
            hexlify(self.user_data.to_bytes(4, 'big')).decode(),
            '\n  '.join([str(x) for x in self.entries])
        )


class KbootAppSector:
    def __init__(self, address):
        if address % 0x1000 != 0:
            raise ValueError('Address must begin at a 4096-byte aligned sector')
        if address in (KbootConstants.STAGE0_ADDRESS, KbootConstants.STAGE1_ADDRESS):
            self.block_size = 0x1000
        elif KbootConstants.APP_ADDRESS_RANGE[0] <= address <= KbootConstants.APP_ADDRESS_RANGE[1]:
            self.block_size = 0x10000
        else:
            raise ValueError('Address must be {}, {}, or between {}'.format(
                hex(KbootConstants.STAGE0_ADDRESS),
                hex(KbootConstants.STAGE1_ADDRESS),
                [hex(x) for x in KbootConstants.APP_ADDRESS_RANGE]
            ))
        self.validate(address)

    def validate(self, address):
        a_block = utils.flash_read(address, self.block_size)
        if a_block[0:1] != b'\x00':
            raise ValueError('AES byte in header at {} must be 0x00'.format(hex(address)))

        expected_size = int.from_bytes(a_block[1:5], 'little')

        hdrapp_hash = sha256()
        app_hash = sha256()
        app_crc = 0
        bytes_read = 0

        begin = 5
        while bytes_read < expected_size:
            if expected_size >= bytes_read + self.block_size:
                end = self.block_size
            else:
                end = expected_size + begin - bytes_read
            
            hdrapp_hash.update(a_block[:end])
            app_hash.update(a_block[begin:end])
            app_crc = crc32(a_block[begin:end], app_crc)
          
            bytes_read += end - begin

            if self.block_size - end < 32:
                begin = 0
                a_block = utils.flash_read(address + bytes_read +5, self.block_size)

        hdrapp_hash = hdrapp_hash.digest()
        if a_block[end:end+32] != hdrapp_hash:
            ValueError('KbootApp at {} is corrupted; calculated sha256 does not match suffix'.format(
                hex(address)
            ))

        if a_block[end+32:] != b'\x00' * (self.block_size - end - 32):
            ValueError('KbootApp at {} should be null-byte padded to the end of the sector'.format(
                hex(address)
            ))

        self.address = address
        self.hdrapp_sha256 = hdrapp_hash
        self.app_sha256 = app_hash.digest()
        self.app_crc32 = app_crc
        self.sector_size = 5 + bytes_read + (self.block_size - end)
        self.app_size = expected_size

    def __str__(self):
        return 'address: {}, sector_size: {}, app_size: {}, crc32: {},\n  app_sha256: {}'.format(
            hex(self.address),
            hex(self.sector_size),
            self.app_size,
            self.app_crc32,
            hexlify(self.app_sha256).decode()
        )


if __name__ == '__main__':

    from binascii import hexlify, unhexlify

    config_tuples = [
        ('main', utils.flash_read(KbootConstants.MAIN_CONFIG_ADDRESS, 4096)),
        ('backup', utils.flash_read(KbootConstants.BACKUP_CONFIG_ADDRESS, 4096))
    ]

    app_tuples = [
        ('stage0', 0x0), 
        ('stage1', 0x1000),
        ('default_app', 0x10000),
        ('firmware_slot1', 0x80000),
        ('firmware_slot2', 0x280000),
        ('firmware_slot3', 0x800000),
    ]

    configs = {}
    for name, raw_bytes in config_tuples:
        config = KbootConfigSector.from_bytes(raw_bytes)
        assert raw_bytes == config.serialize()
        configs[name] = config

    apps = {}
    for name, address in app_tuples:
        try: app = KbootAppSector(address)
        except: continue
        apps[name] = app

    print('\nConfigurations...')
    for name in [x for x,y in config_tuples if x in configs]:
        print('\n KbootConfigSector: {}, {}'.format(name, configs[name]))

    print('\nKboot applications...')
    for name in [x for x,y in app_tuples if x in apps]:
        print('\n KbootAppSector: {}, {}'.format(name, apps[name]))

    entries = []
    for name in [x for x,y in app_tuples if x in apps and x.startswith('firmware')]:
        entries.append(KbootConfigEntry(
            apps[name].address,
            is_active=True,
            ck_crc32=True,
            ck_sha256=True,
            ck_size=True,
            app_size=apps[name].app_size,
            app_crc32=apps[name].app_crc32,
            app_name=name
        ))
    new_config = KbootConfigSector(entries=entries)
    print('\nSuggested KbootConfig: {}'.format(new_config))

Depending on the state of your k210 device's SPI Flash, your output will look different.

In the below example, ktool.py was used to:

  • flash a custom bootloader_hi.bin to 0x1000 which performs sha256 AND crc32 checks for firmwares when both of those configuration entry-flags have been set, just as the documentation states, instead of short-circuiting past the crc32 check if the sha256 check succeeds. If interested, see Kboot:check_crc32_and_sha256
  • firmware at 0x80000 has a bleeding-edge version of krux installed,
  • firmware at 0x280000 has krux v23.09.0 installed,
  • firmware at 0x800000 has krux v22.08.2 installed
  • main and backup configurations have been fiddled, manually via the help of these classes, to support all three firmwares above, but only the first boots by default.

Configurations...

 KbootConfigSector: main, config_flags: 00000000, user_data: 00000000, entries:
  latest flash: flags: active/NO-ck_crc32/ck_sha256/NO-ck_size, address: 0x80000, size: 16384
  krux v23.09.0: flags: active/ck_crc32/ck_sha256/ck_size, address: 0x280000, size: 1902592
  krux v22.08.2: flags: active/ck_crc32/ck_sha256/ck_size, address: 0x800000, size: 1716288

 KbootConfigSector: backup, config_flags: 00000000, user_data: 5aa5d0cf, entries:
  firmware: flags: active/NO-ck_crc32/ck_sha256/NO-ck_size, address: 0x80000, size: 1744896

Kboot applications...

 KbootAppSector: stage0, address: 0x0, sector_size: 0x1000, app_size: 608, crc32: 1848509717,
  app_sha256: 2e050a92efdcb172cb5c6f7cb0b669ba654d2d20219f810fd9fc543f7ef05c3e

 KbootAppSector: stage1, address: 0x1000, sector_size: 0x2000, app_size: 8112, crc32: 2587156063,
  app_sha256: f005f7c8b13aa2719cad58492a8432f14b68b2f845b58e5b5e81ed6309be6172

 KbootAppSector: firmware_slot1, address: 0x80000, sector_size: 0x1e0000, app_size: 1965440, crc32,
  app_sha256: f52c4d5584bdadfaf889edfee7e063064d4c0ae17afd68cfc7be4a33a0f8a3ae

 KbootAppSector: firmware_slot2, address: 0x280000, sector_size: 0x1e0000, app_size: 1902592, crc3,
  app_sha256: 2da65b95435eff19a10fb0d88479bd7a7a86134577a972ab9012790dcac7e3c9

 KbootAppSector: firmware_slot3, address: 0x800000, sector_size: 0x1b0000, app_size: 1716288, crc3,
  app_sha256: 5067429beb16bd0eac55401ea673f55f9cc97585b1a0607d64f8e5f486d4988b

Suggested KbootConfig: config_flags: 00000000, user_data: 00000000, entries:
  firmware_slot1: flags: active/ck_crc32/ck_sha256/ck_size, address: 0x80000, size: 1965440
  firmware_slot2: flags: active/ck_crc32/ck_sha256/ck_size, address: 0x280000, size: 1902592
  firmware_slot3: flags: active/ck_crc32/ck_sha256/ck_size, address: 0x800000, size: 1716288

>>>