src/pqc_bootloader/update_chain.py
| 1 | """UpdateChain - ordered list of SignedFirmware where each links to previous by hash.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from dataclasses import dataclass, field |
| 6 | |
| 7 | from pqc_bootloader.errors import FirmwareRollbackError, UpdateChainError |
| 8 | from pqc_bootloader.firmware import SignedFirmware |
| 9 | |
| 10 | |
| 11 | @dataclass |
| 12 | class UpdateChain: |
| 13 | """Ordered chain of SignedFirmware. |
| 14 | |
| 15 | Each link's `previous_firmware_hash` must equal the prior firmware's |
| 16 | `image_hash`. The chain also enforces non-rollback: new version must be |
| 17 | lexicographically >= previous version (simple semver string compare). |
| 18 | """ |
| 19 | |
| 20 | links: list[SignedFirmware] = field(default_factory=list) |
| 21 | |
| 22 | def add(self, signed: SignedFirmware, allow_rollback: bool = False) -> None: |
| 23 | if self.links: |
| 24 | prev = self.links[-1] |
| 25 | if signed.previous_firmware_hash != prev.firmware.image_hash: |
| 26 | raise UpdateChainError( |
| 27 | f"previous_firmware_hash {signed.previous_firmware_hash[:16]}... does not " |
| 28 | f"match prior image_hash {prev.firmware.image_hash[:16]}..." |
| 29 | ) |
| 30 | if ( |
| 31 | not allow_rollback |
| 32 | and signed.firmware.metadata.version < prev.firmware.metadata.version |
| 33 | ): |
| 34 | raise FirmwareRollbackError( |
| 35 | f"rollback blocked: {signed.firmware.metadata.version} < " |
| 36 | f"{prev.firmware.metadata.version}" |
| 37 | ) |
| 38 | self.links.append(signed) |
| 39 | |
| 40 | def current(self) -> SignedFirmware | None: |
| 41 | return self.links[-1] if self.links else None |
| 42 | |
| 43 | def verify_chain(self) -> tuple[bool, list[str]]: |
| 44 | errors: list[str] = [] |
| 45 | prev_hash: str | None = None |
| 46 | for i, link in enumerate(self.links): |
| 47 | if i > 0 and link.previous_firmware_hash != prev_hash: |
| 48 | errors.append( |
| 49 | f"link break at version {link.firmware.metadata.version}: " |
| 50 | f"expected prev hash {prev_hash}, got {link.previous_firmware_hash}" |
| 51 | ) |
| 52 | prev_hash = link.firmware.image_hash |
| 53 | return len(errors) == 0, errors |
| 54 | |