src/pqc_bootloader/update_chain.py
2.1 KB · 54 lines · python Raw
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