1111from enum import IntEnum , IntFlag , unique
1212from functools import cached_property
1313from io import BufferedReader , BytesIO
14- from typing import Dict , Final , List
14+ from typing import Annotated , Any , Dict , Final , List , Union
1515
1616from intelhex import hex2bin # type: ignore
17+ from pydantic import Field , GetCoreSchemaHandler
1718from pydantic .dataclasses import dataclass
19+ from pydantic_core import CoreSchema , core_schema
1820
1921IMAGE_MAGIC : Final = 0x96F3B83D
2022IMAGE_HEADER_SIZE : Final = 32
@@ -60,12 +62,21 @@ class IMAGE_F(IntFlag):
6062
6163@unique
6264class IMAGE_TLV (IntEnum ):
63- """Image trailer TLV types."""
65+ """Image trailer TLV types.
66+
67+ Specification: https://docs.mcuboot.com/design.html#image-format
68+ """
6469
6570 KEYHASH = 0x01
66- """hash of the public key"""
71+ """Hash of the public key"""
72+ PUBKEY = 0x02
73+ """Public key"""
6774 SHA256 = 0x10
6875 """SHA256 of image hdr and body"""
76+ SHA384 = 0x11
77+ """SHA384 of image hdr and body"""
78+ SHA512 = 0x12
79+ """SHA512 of image hdr and body"""
6980 RSA2048_PSS = 0x20
7081 """RSA2048 of hash output"""
7182 ECDSA224 = 0x21
@@ -76,6 +87,8 @@ class IMAGE_TLV(IntEnum):
7687 """RSA3072 of hash output"""
7788 ED25519 = 0x24
7889 """ED25519 of hash output"""
90+ SIG_PURE = 0x25
91+ """Signature prepared over full image rather than digest"""
7992 ENC_RSA2048 = 0x30
8093 """Key encrypted with RSA-OAEP-2048"""
8194 ENC_KW = 0x31
@@ -84,10 +97,69 @@ class IMAGE_TLV(IntEnum):
8497 """Key encrypted with ECIES-P256"""
8598 ENC_X25519 = 0x33
8699 """Key encrypted with ECIES-X25519"""
100+ ENC_X25519_SHA512 = 0x34
101+ """Key exchange using X25519 with SHA512 MAC"""
87102 DEPENDENCY = 0x40
88103 """Image depends on other image"""
89104 SEC_CNT = 0x50
90- """security counter"""
105+ """Security counter"""
106+ BOOT_RECORD = 0x60
107+ """Measured boot record"""
108+ DECOMP_SIZE = 0x70
109+ """Decompressed image size excluding header/TLVs"""
110+ DECOMP_SHA = 0x71
111+ """Decompressed image hash matching format of compressed slot"""
112+ DECOMP_SIGNATURE = 0x72
113+ """Decompressed image signature matching compressed format"""
114+ COMP_DEC_SIZE = 0x73
115+ """Compressed decrypted image size"""
116+ UUID_VID = 0x80
117+ """Vendor unique identifier"""
118+ UUID_CID = 0x81
119+ """Device class unique identifier"""
120+
121+
122+ class VendorTLV (int ):
123+ """Vendor-defined TLV type in reserved ranges (0xXXA0-0xXXFE).
124+
125+ Vendor reserved TLVs occupy ranges from 0xXXA0 to 0xXXFE, where XX
126+ represents any upper byte value. Examples include ranges 0x00A0-0x00FF,
127+ 0x01A0-0x01FF, and 0x02A0-0x02FF, continuing through 0xFFA0-0xFFFE.
128+ """
129+
130+ def __new__ (cls , value : int ) -> 'VendorTLV' :
131+ """Create a new VendorTLV, validating the range."""
132+ lower_byte = value & 0xFF
133+ if not (0xA0 <= lower_byte <= 0xFE ):
134+ raise ValueError (
135+ f"VendorTLV 0x{ value :02x} must have lower byte in range 0xA0-0xFE, "
136+ f"got 0x{ lower_byte :02x} "
137+ )
138+ return int .__new__ (cls , value )
139+
140+ @classmethod
141+ def __get_pydantic_core_schema__ (
142+ cls , _source_type : Any , _handler : GetCoreSchemaHandler
143+ ) -> CoreSchema :
144+ def validate (value : int ) -> VendorTLV :
145+ return cls (value )
146+
147+ return core_schema .no_info_after_validator_function (
148+ validate ,
149+ core_schema .int_schema (),
150+ )
151+
152+
153+ ImageTLVType = Annotated [Union [IMAGE_TLV , VendorTLV , int ], Field (union_mode = "left_to_right" )]
154+ """TLV type that accepts standard IMAGE_TLV enums, vendor-defined TLVs, or any integer.
155+
156+ This uses Pydantic's "left to right" union mode to:
157+ 1. First try to match against IMAGE_TLV enum values
158+ 2. Then try to validate as a VendorTLV (0xXXA0-0xXXFE ranges)
159+ 3. Finally accept any integer as a fallback
160+
161+ This ensures backward compatibility and supports future TLV types without validation errors.
162+ """
91163
92164
93165@dataclass (frozen = True )
@@ -189,7 +261,7 @@ def load_from(file: BytesIO | BufferedReader) -> 'ImageTLVInfo':
189261class ImageTLV :
190262 """A TLV header - type and length."""
191263
192- type : IMAGE_TLV
264+ type : ImageTLVType
193265 len : int
194266 """Data length (not including TLV header)."""
195267
@@ -209,7 +281,12 @@ def __post_init__(self) -> None:
209281 raise MCUBootImageError (f"TLV requires length { self .header .len } , got { len (self .value )} " )
210282
211283 def __str__ (self ) -> str :
212- return f"{ self .header .type .name } ={ self .value .hex ()} "
284+ type_name = (
285+ self .header .type .name
286+ if isinstance (self .header .type , IMAGE_TLV )
287+ else f"0x{ self .header .type :02x} "
288+ )
289+ return f"{ type_name } ={ self .value .hex ()} "
213290
214291
215292@dataclass (frozen = True )
@@ -221,7 +298,7 @@ class ImageInfo:
221298 tlvs : List [ImageTLVValue ]
222299 file : str | None = None
223300
224- def get_tlv (self , tlv : IMAGE_TLV ) -> ImageTLVValue :
301+ def get_tlv (self , tlv : ImageTLVType ) -> ImageTLVValue :
225302 """Get a TLV from the image or raise `TLVNotFound`."""
226303 if tlv in self ._map_tlv_type_to_value :
227304 return self ._map_tlv_type_to_value [tlv ]
@@ -263,7 +340,7 @@ def load_file(path: str) -> 'ImageInfo':
263340 return ImageInfo (file = path , header = image_header , tlv_info = tlv_info , tlvs = tlvs )
264341
265342 @cached_property
266- def _map_tlv_type_to_value (self ) -> Dict [IMAGE_TLV , ImageTLVValue ]:
343+ def _map_tlv_type_to_value (self ) -> Dict [int , ImageTLVValue ]:
267344 return {tlv .header .type : tlv for tlv in self .tlvs }
268345
269346 def __str__ (self ) -> str :
0 commit comments