amd-ucode-patch 1.0.0__tar.gz → 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/PKG-INFO +4 -2
  2. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/pyproject.toml +6 -3
  3. amd_ucode_patch-1.0.0/src/amd_ucode_patch/cli_info.py → amd_ucode_patch-1.0.1/src/amd_ucode_patch/cli/info.py +23 -13
  4. amd_ucode_patch-1.0.1/src/amd_ucode_patch/cli/sign.py +89 -0
  5. amd_ucode_patch-1.0.1/src/amd_ucode_patch/naming.py +16 -0
  6. amd_ucode_patch-1.0.1/src/amd_ucode_patch/parse.py +9 -0
  7. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/body.py +45 -0
  8. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch.py +36 -0
  9. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_date.py +90 -0
  10. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_header.py +121 -0
  11. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_level.py +280 -0
  12. amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/signature.py +68 -0
  13. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/src/amd_ucode_patch/structures/verified_header.py +10 -10
  14. amd_ucode_patch-1.0.1/src/amd_ucode_patch/utils/bcd.py +48 -0
  15. amd_ucode_patch-1.0.1/src/amd_ucode_patch/utils/rsa.py +65 -0
  16. amd_ucode_patch-1.0.0/src/amd_ucode_patch/naming.py +0 -15
  17. amd_ucode_patch-1.0.0/src/amd_ucode_patch/parse.py +0 -9
  18. amd_ucode_patch-1.0.0/src/amd_ucode_patch/structures/ucode_patch.py +0 -29
  19. amd_ucode_patch-1.0.0/src/amd_ucode_patch/structures/ucode_patch_header.py +0 -126
  20. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/.github/dependabot.yml +0 -0
  21. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/.github/workflows/build.yml +0 -0
  22. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/.github/workflows/codeql.yml +0 -0
  23. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/.github/workflows/scorecard.yml +0 -0
  24. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/.gitignore +0 -0
  25. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/LICENSE.md +0 -0
  26. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/README.md +0 -0
  27. {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.0.1}/src/amd_ucode_patch/banner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amd-ucode-patch
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: A Python library for parsing and interpreting AMD microcode patch files
5
5
  Project-URL: Homepage, https://github.com/amd-zenith/amd-ucode-patch
6
6
  Project-URL: Repository, https://github.com/amd-zenith/amd-ucode-patch
@@ -23,7 +23,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: System :: Hardware
24
24
  Classifier: Topic :: Utilities
25
25
  Requires-Python: >=3.10
26
- Requires-Dist: rich
26
+ Requires-Dist: amd-cpuid==1.0.1
27
+ Requires-Dist: pycryptodome==3.23.0
28
+ Requires-Dist: rich==15.0.0
27
29
  Description-Content-Type: text/markdown
28
30
 
29
31
  # AMD Microcode Patch
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling", "hatch-vcs"]
2
+ requires = ["hatchling==1.30.1", "hatch-vcs==0.5.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -30,7 +30,9 @@ classifiers = [
30
30
  "Topic :: Utilities",
31
31
  ]
32
32
  dependencies = [
33
- "rich",
33
+ "amd-cpuid==1.0.1",
34
+ "pycryptodome==3.23.0",
35
+ "rich==15.0.0",
34
36
  ]
35
37
 
36
38
  [project.urls]
@@ -39,7 +41,8 @@ Repository = "https://github.com/amd-zenith/amd-ucode-patch"
39
41
  Issues = "https://github.com/amd-zenith/amd-ucode-patch/issues"
40
42
 
41
43
  [project.scripts]
42
- amd_ucode_patch_info = "amd_ucode_patch.cli_info:main"
44
+ amd_ucode_patch_info = "amd_ucode_patch.cli.info:main"
45
+ amd_ucode_patch_sign = "amd_ucode_patch.cli.sign:main"
43
46
 
44
47
  [tool.hatch.version]
45
48
  source = "vcs"
@@ -10,10 +10,11 @@ from pathlib import Path
10
10
  from rich import box
11
11
  from rich.console import Console
12
12
  from rich.table import Table
13
- from .banner import BANNER
14
- from .parse import ucode_patch_parse
13
+ from ..banner import BANNER
14
+ from ..parse import ucode_patch_parse
15
+ from ..structures.patch_level import PatchLevelV2
15
16
 
16
- COLS = ["File", "Date", "Upd. Rev", "Loader ID", "Proc. Rev", "CPUID", "Family", "Model", "Stepping", "Autorun", "Encrypted", "Body size"]
17
+ COLS = ["File", "Date", "Patch level", "PL Ver", "PL Rev", "Loader ID", "Proc. Rev", "CPUID", "Family", "Family Name", "Model", "Stepping", "Microarch", "Codename", "Signed", "Autorun", "Encrypted", "Body size"]
17
18
 
18
19
 
19
20
  def expand_paths(patterns):
@@ -27,19 +28,28 @@ def expand_paths(patterns):
27
28
 
28
29
 
29
30
  def _row_fields(path, patch):
31
+ patch_level = patch.header.patch_level
32
+ # Advanced (v2 / Zen+) patch level fields are only meaningful when present.
33
+ patch_rev = f"{patch_level.rev:02x}" if isinstance(patch_level, PatchLevelV2) else ""
30
34
  return (
31
35
  str(path),
32
- f"{patch.header.year:04d}-{patch.header.month:02d}-{patch.header.day:02d}",
33
- f"{patch.header.update_revision:08x}",
36
+ str(patch.header.date),
37
+ str(patch_level),
38
+ f"v{int(patch_level.version)}",
39
+ patch_rev,
34
40
  f"{patch.header.loader_id:04x}",
35
- f"{patch.header.processor_rev_id:04x}",
36
- patch.header.cpuid_str,
37
- f"0x{patch.header.cpu_family:02x}",
38
- f"0x{patch.header.cpu_model:02x}",
39
- f"0x{patch.header.cpu_stepping:02x}",
40
- f"{patch.verified_header.autorun if patch.verified_header is not None else ''}",
41
- f"{patch.verified_header.encrypted if patch.verified_header is not None else ''}",
42
- f"{len(patch.body)}",
41
+ f"{patch.header.cpuid.ucode_signature:04x}",
42
+ f"{patch.header.cpuid.cpuid_signature:08X}",
43
+ f"0x{patch.header.cpuid.family:02x}",
44
+ patch.header.cpuid.familyname,
45
+ f"0x{patch.header.cpuid.model:02x}",
46
+ f"0x{patch.header.cpuid.stepping:02x}",
47
+ patch.header.cpuid.microarchitecture,
48
+ patch.header.cpuid.codename,
49
+ "yes" if patch.header.signature is not None else "no",
50
+ f"{patch.body.verified_header.autorun if patch.body.verified_header is not None else ''}",
51
+ f"{patch.body.verified_header.encrypted if patch.body.verified_header is not None else ''}",
52
+ f"{len(patch.body.data)}",
43
53
  )
44
54
 
45
55
  def print_table(console: Console, paths, format):
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+ '''
4
+ A command line tool to inspect and verify AMD uCode patch signatures.
5
+ '''
6
+
7
+ import sys
8
+ import argparse
9
+ from Crypto.Cipher import AES
10
+ from Crypto.Hash import CMAC
11
+ from rich import box
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from ..banner import BANNER
15
+ from ..parse import ucode_patch_parse
16
+ from .info import expand_paths
17
+
18
+ COLS = ["File", "Signed", "Digest", "Valid"]
19
+
20
+
21
+ def _parse_key(value: str) -> bytes:
22
+ text = value.strip().lower()
23
+ if text.startswith("0x"):
24
+ text = text[2:]
25
+ try:
26
+ key = bytes.fromhex(text)
27
+ except ValueError:
28
+ raise argparse.ArgumentTypeError(f"key must be hexadecimal, got {value!r}")
29
+ if len(key) != 16:
30
+ raise argparse.ArgumentTypeError(
31
+ f"key must be 16 bytes (32 hex chars), got {len(key)}"
32
+ )
33
+ return key
34
+
35
+
36
+ def _row(path, key):
37
+ """Return (row_cells, verdict) where verdict is True/False (key given) or None."""
38
+ patch = ucode_patch_parse(path)
39
+ signature = patch.header.signature
40
+
41
+ if signature is None:
42
+ return (str(path), "[yellow]no[/yellow]", "", "[dim]n/a[/dim]"), None
43
+
44
+ recovered = signature.recover_digest()
45
+ digest = recovered.hex() if recovered is not None else "[red]<bad padding>[/red]"
46
+
47
+ if key is None:
48
+ return (str(path), "[green]yes[/green]", digest, "[dim]not checked[/dim]"), None
49
+
50
+ computed = CMAC.new(key, msg=patch.body.to_bytes(), ciphermod=AES).digest()
51
+ verdict = recovered is not None and recovered == computed
52
+ valid = "[green]yes[/green]" if verdict else "[red]no[/red]"
53
+ return (str(path), "[green]yes[/green]", digest, valid), verdict
54
+
55
+
56
+ def main():
57
+ console = Console()
58
+ console.print(BANNER, highlight=False)
59
+
60
+ parser = argparse.ArgumentParser(
61
+ description="Inspect AMD microcode patch signatures; verify them when a key is provided.",
62
+ epilog="The published Zen 1-4 CMAC key is 2b7e151628aed2a6abf7158809cf4f3c.",
63
+ )
64
+ parser.add_argument("files", nargs="+", help="Patch files to inspect")
65
+ parser.add_argument(
66
+ "-k", "--key", type=_parse_key, default=None,
67
+ help="AES-128 CMAC key as hex (32 hex chars); enables signature verification",
68
+ )
69
+ args = parser.parse_args()
70
+
71
+ table = Table(*COLS, box=box.HEAVY_HEAD)
72
+ all_ok = True
73
+ for path in expand_paths(args.files):
74
+ try:
75
+ row, verdict = _row(path, args.key)
76
+ table.add_row(*row)
77
+ if verdict is False:
78
+ all_ok = False
79
+ except Exception as e:
80
+ console.log(f"Error reading {path}: {e}")
81
+ all_ok = False
82
+ console.print(table)
83
+
84
+ # Non-zero only when a key was given and a signature failed to verify.
85
+ sys.exit(0 if all_ok else 1)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ from .structures.patch import Patch
5
+
6
+ def ucode_patch_name(patch: Patch, enc_override=None) -> str:
7
+ # Format should be:
8
+ # family<family>_cpuid<cpuid>_rev<revision>_date<yyyymmdd>_enc<ee>.bin
9
+ # The _enc<ee> suffix is only present when a verified header exists.
10
+ cpuid = patch.header.cpuid
11
+ name = f"family{cpuid.family:02x}_cpuid{cpuid.cpuid_signature:08X}_rev{patch.header.patch_level}_date{patch.header.date.year:04}{patch.header.date.month:02}{patch.header.date.day:02}"
12
+ if patch.body.verified_header is not None:
13
+ enc = patch.body.verified_header.encrypted if enc_override is None else enc_override
14
+ name += f"_enc{enc:02}"
15
+ name += ".bin"
16
+ return name
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ from pathlib import Path
5
+ from .structures.patch import Patch
6
+
7
+ def ucode_patch_parse(file: Path) -> Patch:
8
+ with file.open("rb") as f:
9
+ return Patch.from_bytes(f.read())
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ from dataclasses import dataclass
5
+ from .verified_header import VerifiedHeader
6
+
7
+
8
+ #: Family at/after which the body carries a verified header (options + rev).
9
+ #: This is the Zen boundary; kept independent of the signature gating.
10
+ VERIFIED_HEADER_MIN_FAMILY = 0x17
11
+
12
+
13
+ @dataclass
14
+ class Body:
15
+ """
16
+ The signed portion of an AMD microcode patch -- the region the RSA signature
17
+ in :class:`PatchHeader` covers (through the AES-CMAC).
18
+
19
+ On Zen (signed) patches it is the ``options``/``rev`` block
20
+ (:class:`VerifiedHeader`) followed by the match registers and the (encrypted)
21
+ microcode opquads. On pre-Zen patches there is no verified header and the
22
+ whole thing is the raw, unsigned microcode.
23
+ """
24
+
25
+ verified_header: VerifiedHeader | None
26
+ data: bytes
27
+
28
+ @staticmethod
29
+ def from_bytes(buf: bytes, family: int | None = None) -> "Body":
30
+ """
31
+ Parse the body. The verified header (options + rev) is present only on
32
+ Zen patches (family >= ``VERIFIED_HEADER_MIN_FAMILY``); ``family`` selects
33
+ that. When ``family`` is unknown the body is treated as having none.
34
+ """
35
+ if family is not None and family >= VERIFIED_HEADER_MIN_FAMILY:
36
+ verified_header = VerifiedHeader.from_bytes(buf, family=family)
37
+ data = buf[VerifiedHeader.SIZE:]
38
+ else:
39
+ verified_header = None
40
+ data = buf
41
+ return Body(verified_header=verified_header, data=data)
42
+
43
+ def to_bytes(self) -> bytes:
44
+ prefix = self.verified_header.to_bytes() if self.verified_header is not None else b""
45
+ return prefix + self.data
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ from dataclasses import dataclass
5
+ from .patch_header import PatchHeader
6
+ from .body import Body
7
+
8
+
9
+ @dataclass
10
+ class Patch:
11
+ header: PatchHeader
12
+ body: Body
13
+
14
+
15
+ @staticmethod
16
+ def from_bytes(buf: bytes) -> "Patch":
17
+ header = PatchHeader.from_bytes(buf)
18
+ # Everything after the header is the signed body: the verified header
19
+ # (options + rev, Zen only) plus the microcode itself.
20
+ body = Body.from_bytes(buf[header.size:], family=header.cpuid.family)
21
+ return Patch(header=header, body=body)
22
+
23
+ def signature_verifies(self, cmac_key: bytes) -> bool | None:
24
+ """
25
+ Whether the patch signature verifies against its embedded public key,
26
+ using the provided CMAC key (see :data:`Signature.verify`).
27
+
28
+ ``None`` for unsigned (pre-Zen) patches. A ``False`` result does not by
29
+ itself mean the patch is forged: patches signed with a CMAC key this
30
+ library does not ship -- Zen 5, and AMD's post-EntrySign patches that
31
+ rotated the key -- also report ``False`` even though their RSA signature
32
+ is structurally valid against the embedded modulus.
33
+ """
34
+ if self.header.signature is None:
35
+ return None
36
+ return self.header.signature.verify(self.body.to_bytes(), cmac_key)
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ import struct
5
+ from dataclasses import dataclass
6
+
7
+ from amd_ucode_patch.utils.bcd import decode_bcd, encode_bcd, is_bcd
8
+
9
+
10
+ @dataclass
11
+ class PatchDate:
12
+ """
13
+ Build date stored in the first 32-bit word of an AMD microcode patch header.
14
+
15
+ The field is a little-endian, packed binary-coded-decimal value laid out as
16
+ 0xMMDDYYYY: the high byte holds the month, the next byte the day, and the low
17
+ 16 bits the four-digit year (e.g. 0x12282021 decodes to 2021-12-28).
18
+ """
19
+
20
+ FMT = "<HBB" # year (u16), day (u8), month (u8)
21
+ SIZE = struct.calcsize(FMT)
22
+
23
+ YEAR_NIBBLES = 4
24
+ DAY_NIBBLES = 2
25
+ MONTH_NIBBLES = 2
26
+ NIBBLE_COUNT = YEAR_NIBBLES + DAY_NIBBLES + MONTH_NIBBLES
27
+
28
+ # Loose calendar bounds, used only to reject obviously bogus values when
29
+ # probing a file.
30
+ MIN_YEAR = 1990
31
+ MAX_YEAR = 2099
32
+
33
+ # Known AMD patches that ship an out-of-range date but are otherwise
34
+ # legitimate. Treated as plausible despite failing the calendar checks.
35
+ # 2011-13-09 -> family12_cpuid00300F10_rev03000027 (month 13).
36
+ KNOWN_EXCEPTIONS = frozenset({(2011, 13, 9)})
37
+
38
+ year: int
39
+ month: int
40
+ day: int
41
+
42
+ @staticmethod
43
+ def from_bytes(buf: bytes) -> "PatchDate":
44
+ if len(buf) < PatchDate.SIZE:
45
+ raise ValueError("not enough bytes for AMD patch date")
46
+ raw_year, raw_day, raw_month = struct.unpack(PatchDate.FMT, buf[0:PatchDate.SIZE])
47
+ return PatchDate(
48
+ year=decode_bcd(raw_year, PatchDate.YEAR_NIBBLES),
49
+ day=decode_bcd(raw_day, PatchDate.DAY_NIBBLES),
50
+ month=decode_bcd(raw_month, PatchDate.MONTH_NIBBLES),
51
+ )
52
+
53
+ def to_bytes(self) -> bytes:
54
+ return struct.pack(
55
+ PatchDate.FMT,
56
+ encode_bcd(self.year, PatchDate.YEAR_NIBBLES),
57
+ encode_bcd(self.day, PatchDate.DAY_NIBBLES),
58
+ encode_bcd(self.month, PatchDate.MONTH_NIBBLES),
59
+ )
60
+
61
+ @staticmethod
62
+ def is_valid_encoding(buf: bytes) -> bool:
63
+ """
64
+ Return ``True`` if ``buf`` holds well-formed packed BCD that decodes to a
65
+ plausible calendar date. Intended as a lightweight signature check while
66
+ deciding whether a file looks like a microcode patch.
67
+ """
68
+ if len(buf) < PatchDate.SIZE:
69
+ return False
70
+ raw_year, raw_day, raw_month = struct.unpack(PatchDate.FMT, buf[0:PatchDate.SIZE])
71
+ if not (
72
+ is_bcd(raw_year, PatchDate.YEAR_NIBBLES)
73
+ and is_bcd(raw_day, PatchDate.DAY_NIBBLES)
74
+ and is_bcd(raw_month, PatchDate.MONTH_NIBBLES)
75
+ ):
76
+ return False
77
+ return PatchDate.from_bytes(buf).is_plausible()
78
+
79
+ def is_plausible(self) -> bool:
80
+ """Return ``True`` if the components fall within sane calendar ranges."""
81
+ if (self.year, self.month, self.day) in PatchDate.KNOWN_EXCEPTIONS:
82
+ return True
83
+ return (
84
+ 1 <= self.month <= 12
85
+ and 1 <= self.day <= 31
86
+ and PatchDate.MIN_YEAR <= self.year <= PatchDate.MAX_YEAR
87
+ )
88
+
89
+ def __str__(self) -> str:
90
+ return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ import struct
5
+ from dataclasses import dataclass
6
+ from amd_cpuid import AmdCpuId
7
+ from amd_ucode_patch.structures.patch_date import PatchDate
8
+ from amd_ucode_patch.structures.patch_level import PatchLevel
9
+ from amd_ucode_patch.structures.signature import Signature
10
+
11
+
12
+ #: Patches are cryptographically signed starting with Zen (family 0x17). Earlier
13
+ #: families carry no signature block.
14
+ SIGNED_MIN_FAMILY = 0x17
15
+
16
+
17
+ @dataclass
18
+ class PatchHeader:
19
+ '''
20
+ AMD microcode patch header.
21
+
22
+ The fixed 32-byte core is the Linux ``microcode_header_amd``. The fields the
23
+ project interprets are:
24
+
25
+ - ``date`` -- build date (``data_code``; a BCD date, not a code)
26
+ - ``patch_level`` -- patch id / revision (``patch_id``; the value ``rdmsr
27
+ 0x8B`` reports for the loaded microcode)
28
+ - ``loader_id`` -- microcode loader id
29
+ - ``cpuid`` -- target processor, decoded from the ucode signature
30
+ (``processor_rev_id``)
31
+
32
+ On Zen and later (family >= 0x17) a 768-byte cryptographic ``Signature`` block.
33
+
34
+ The header has two sizes: ``SIZE`` is the fixed core (32 bytes) and the
35
+ ``size`` property is the total encoding (core + signature block when present).
36
+ '''
37
+ # Fields after the leading PatchDate (data_code) and PatchLevel (patch_id).
38
+ FMT = "<HBB I II H BBBBBB"
39
+ # Size of the fixed core struct (the Linux ``microcode_header_amd``). The
40
+ # optional Zen signature block follows it; use ``size`` for the total.
41
+ SIZE = PatchDate.SIZE + PatchLevel.SIZE + struct.calcsize(FMT)
42
+
43
+ date: PatchDate
44
+ patch_level: PatchLevel
45
+ loader_id: int
46
+ unk0: int
47
+ unk1: int
48
+ unk2: int
49
+ unk3: int
50
+ unk4: int
51
+ cpuid: AmdCpuId
52
+ unk5: int
53
+ unk6: int
54
+ unk7: int
55
+ unk8: int
56
+ unk9: int
57
+ unk10: int
58
+ #: Cryptographic signature block; present only on Zen (family >= 0x17).
59
+ signature: Signature | None
60
+
61
+ @staticmethod
62
+ def from_bytes(buf: bytes) -> "PatchHeader":
63
+ if len(buf) < PatchHeader.SIZE:
64
+ raise ValueError("not enough bytes for AMD header")
65
+ date = PatchDate.from_bytes(buf)
66
+ rest_start = PatchDate.SIZE + PatchLevel.SIZE
67
+ chunk = buf[rest_start:PatchHeader.SIZE]
68
+ vals = struct.unpack(PatchHeader.FMT, chunk)
69
+ cpuid = AmdCpuId.from_ucode_signature(vals[6])
70
+ # The patch level layout depends on the CPU family, so build cpuid first.
71
+ patch_level = PatchLevel.from_bytes(buf[PatchDate.SIZE:], family=cpuid.family)
72
+ # The signature block follows the core struct, on Zen and later only.
73
+ signature = (
74
+ Signature.from_bytes(buf[PatchHeader.SIZE:])
75
+ if cpuid.family >= SIGNED_MIN_FAMILY
76
+ else None
77
+ )
78
+ return PatchHeader(
79
+ date=date,
80
+ patch_level=patch_level,
81
+ loader_id=vals[0],
82
+ unk0=vals[1],
83
+ unk1=vals[2],
84
+ unk2=vals[3],
85
+ unk3=vals[4],
86
+ unk4=vals[5],
87
+ cpuid=cpuid,
88
+ unk5=vals[7],
89
+ unk6=vals[8],
90
+ unk7=vals[9],
91
+ unk8=vals[10],
92
+ unk9=vals[11],
93
+ unk10=vals[12],
94
+ signature=signature,
95
+ )
96
+
97
+ @property
98
+ def size(self) -> int:
99
+ """Total encoded size: the core struct plus the signature block if present."""
100
+ return PatchHeader.SIZE + (Signature.SIZE if self.signature is not None else 0)
101
+
102
+ def to_bytes(self) -> bytes:
103
+ core = self.date.to_bytes() + self.patch_level.to_bytes() + struct.pack(
104
+ PatchHeader.FMT,
105
+ self.loader_id,
106
+ self.unk0,
107
+ self.unk1,
108
+ self.unk2,
109
+ self.unk3,
110
+ self.unk4,
111
+ self.cpuid.ucode_signature,
112
+ self.unk5,
113
+ self.unk6,
114
+ self.unk7,
115
+ self.unk8,
116
+ self.unk9,
117
+ self.unk10,
118
+ )
119
+ if self.signature is not None:
120
+ core += self.signature.to_bytes()
121
+ return core
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ import struct
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from enum import IntEnum
8
+ from typing import ClassVar
9
+
10
+
11
+ #: Effective family at which the v2 (Zen) layout applies. The Linux kernel
12
+ #: switches behaviour on ``x86_family(bsp_cpuid_1_eax) >= 0x17``.
13
+ V2_MIN_FAMILY = 0x17
14
+
15
+
16
+ class PatchLevelError(Exception):
17
+ """Base error for patch level operations."""
18
+
19
+
20
+ class PatchLevelMismatchError(PatchLevelError):
21
+ """Raised when comparing two v2 patch levels that target different CPUs."""
22
+
23
+
24
+ class PatchLevelVersion(IntEnum):
25
+ """Which structural layout a :class:`PatchLevel` uses (see module docs)."""
26
+
27
+ V1 = 1
28
+ V2 = 2
29
+
30
+ def __str__(self) -> str:
31
+ return {
32
+ PatchLevelVersion.V1: "v1 (pre-Zen)",
33
+ PatchLevelVersion.V2: "v2 (Zen+)",
34
+ }[self]
35
+
36
+
37
+ @dataclass
38
+ class PatchLevel(ABC):
39
+ """
40
+ Microcode patch level (a.k.a. patch ID / update revision), the second 32-bit
41
+ word of an AMD microcode patch header.
42
+
43
+ Names across sources, all the same ``u32`` at header offset 4:
44
+
45
+ - ``patch_id`` -- Linux ``struct microcode_header_amd``
46
+ - ``revision`` -- zentool ``struct ucodehdr``
47
+ - ``ucode_level`` / "Patch level" -- AMD ``amd_ucode_info.py`` / linux-firmware
48
+ - ``MSR_AMD64_PATCH_LEVEL`` (MSR ``0x8B``) -- what the CPU reports for the
49
+ currently loaded patch; the kernel verifies ``rdmsr 0x8B == hdr.patch_id``
50
+ after applying a patch.
51
+
52
+ The value is compared as a whole word by the loader, but it is not opaque.
53
+ It is a structured patch identifier whose layout depends on the CPU
54
+ generation. There are two documented versions ("v1" pre-Zen, "v2" Zen).
55
+
56
+ --------------------------------------------------------------------------
57
+ Version 1 -- pre-Zen (family <= 0x16)
58
+ --------------------------------------------------------------------------
59
+ The patch level is an *opaque, monotonically increasing counter*. The CPU
60
+ that a patch targets is NOT encoded here; matching is done via a separate
61
+ equivalence table (``equiv_cpu_entry``) keyed on ``CPUID(1).EAX``, and the
62
+ header's ``processor_rev_id`` links each patch to that table.
63
+
64
+ Apply rule (kernel): whole-word compare -- ``n->patch_id > p->patch_id``.
65
+
66
+ No source documents an internal sub-byte layout for v1. Empirically over the
67
+ collection the top byte still tracks the extended family
68
+ (``patch_id[31:24] == cpuid.familyext``), but the low 24 bits are just a
69
+ counter and bits ``[19:16]`` are NOT reserved (some v1 patches set them).
70
+ Do not decode model/stepping from a v1 patch level.
71
+
72
+ --------------------------------------------------------------------------
73
+ Version 2 -- Zen and later (family >= 0x17)
74
+ --------------------------------------------------------------------------
75
+ The patch level hardcodes the target family/model/stepping so the loader can
76
+ skip the equivalence table entirely. Bit layout (Linux ``union
77
+ zen_patch_rev``)::
78
+
79
+ bits [ 7: 0] rev patch sequence number (the only part that
80
+ increments between updates for a given CPU)
81
+ bits [11: 8] stepping CPU stepping
82
+ bits [15:12] model model, low nibble
83
+ bits [19:16] (reserved) always 0
84
+ bits [23:20] ext_model model, high nibble
85
+ bits [31:24] ext_fam extended family (effective family - 0xF)
86
+
87
+ Effective family = ``0xF + ext_fam``; effective model =
88
+ ``(ext_model << 4) | model``. These upper three bytes mirror
89
+ ``CPUID(1).EAX`` (base family nibble is forced to ``0xF`` and reconstructed).
90
+
91
+ Apply rule (kernel ``patch_newer``): the stepping must match, then compare
92
+ only the low byte -- ``zn.rev > zp.rev``.
93
+
94
+ --------------------------------------------------------------------------
95
+ References
96
+ --------------------------------------------------------------------------
97
+ - Linux ``arch/x86/kernel/cpu/microcode/amd.c`` -- ``struct
98
+ microcode_header_amd``, ``union zen_patch_rev``, ``patch_newer()``,
99
+ ``equiv_cpu_entry``, ``MSR_AMD64_PATCH_LEVEL``.
100
+ - AMD ``amd_ucode_info.py`` (AMDESE) and linux-firmware ``amd-ucode/README``.
101
+ - AMD CPUID Specification (PDF 25481) for the ``CPUID(1).EAX`` f/m/s layout.
102
+ """
103
+
104
+ FMT = "<I"
105
+ SIZE = struct.calcsize(FMT)
106
+
107
+ #: Layout this instance uses; set by each concrete subclass.
108
+ version: ClassVar[PatchLevelVersion]
109
+
110
+ value: int
111
+
112
+ @staticmethod
113
+ def from_bytes(buf: bytes, family: int | None = None) -> "PatchLevel":
114
+ """
115
+ Parse a patch level from the first 4 bytes of ``buf``.
116
+
117
+ ``family`` is the effective CPU family (from the header's cpuid). It
118
+ selects the layout: ``family >= 0x17`` -> v2, otherwise v1. When it is
119
+ unknown (``None``) the value defaults to the v1 (opaque counter) layout.
120
+ """
121
+ if len(buf) < PatchLevel.SIZE:
122
+ raise ValueError("not enough bytes for AMD patch level")
123
+ (value,) = struct.unpack(PatchLevel.FMT, buf[0:PatchLevel.SIZE])
124
+ return PatchLevel.from_value(value, family)
125
+
126
+ @staticmethod
127
+ def from_value(value: int, family: int | None = None) -> "PatchLevel":
128
+ """Build the variant appropriate for ``family`` (see :meth:`from_bytes`)."""
129
+ if family is not None and family >= V2_MIN_FAMILY:
130
+ return PatchLevelV2(value)
131
+ return PatchLevelV1(value)
132
+
133
+ def to_bytes(self) -> bytes:
134
+ return struct.pack(PatchLevel.FMT, self.value)
135
+
136
+ @abstractmethod
137
+ def is_valid(self, cpuid=None) -> bool:
138
+ """
139
+ Whether the structured fields are self-consistent (and, if ``cpuid`` is
140
+ given, consistent with it). See the concrete variants for the rules.
141
+ """
142
+
143
+ @abstractmethod
144
+ def _cmp_same(self, other: "PatchLevel") -> int:
145
+ """Compare against a same-version instance; return -1, 0 or 1."""
146
+
147
+ def _compare(self, other: object):
148
+ if not isinstance(other, PatchLevel):
149
+ return NotImplemented
150
+ if type(self) is not type(other):
151
+ raise PatchLevelError(
152
+ f"cannot compare {self.version} with {other.version} patch levels"
153
+ )
154
+ return self._cmp_same(other)
155
+
156
+ def __lt__(self, other: object):
157
+ r = self._compare(other)
158
+ return r < 0 if r is not NotImplemented else NotImplemented
159
+
160
+ def __le__(self, other: object):
161
+ r = self._compare(other)
162
+ return r <= 0 if r is not NotImplemented else NotImplemented
163
+
164
+ def __gt__(self, other: object):
165
+ r = self._compare(other)
166
+ return r > 0 if r is not NotImplemented else NotImplemented
167
+
168
+ def __ge__(self, other: object):
169
+ r = self._compare(other)
170
+ return r >= 0 if r is not NotImplemented else NotImplemented
171
+
172
+ def __int__(self) -> int:
173
+ return self.value
174
+
175
+ def __str__(self) -> str:
176
+ return f"{self.value:08x}"
177
+
178
+
179
+ @dataclass
180
+ class PatchLevelV1(PatchLevel):
181
+ """
182
+ Pre-Zen (family <= 0x16) patch level
183
+ An opaque, monotonically increasing counter.
184
+ """
185
+
186
+ version: ClassVar[PatchLevelVersion] = PatchLevelVersion.V1
187
+
188
+ @property
189
+ def rev(self) -> int:
190
+ """The monotonic patch counter."""
191
+ return self.value
192
+
193
+ def is_valid(self, cpuid=None) -> bool:
194
+ # v1 has no documented internal structure to validate. The top byte
195
+ # only advisorily tracks the extended family and a few genuine early
196
+ # patches violate even that, so validity is not gated on it.
197
+ return True
198
+
199
+ def _cmp_same(self, other: "PatchLevel") -> int:
200
+ return (self.value > other.value) - (self.value < other.value)
201
+
202
+
203
+ @dataclass
204
+ class PatchLevelV2(PatchLevel):
205
+ """
206
+ Zen and later (family >= 0x17) patch level.
207
+ The whole word decodes (Linux ``union zen_patch_rev``); the upper three bytes
208
+ identify the target CPU and only :attr:`rev` (low byte) increments between
209
+ updates.
210
+
211
+ Comparison requires the same target CPU: comparing two v2 patch levels whose
212
+ upper three bytes differ raises :class:`PatchLevelMismatchError`; otherwise
213
+ the low :attr:`rev` byte is compared.
214
+ """
215
+
216
+ version: ClassVar[PatchLevelVersion] = PatchLevelVersion.V2
217
+
218
+ @property
219
+ def rev(self) -> int:
220
+ """Patch sequence number (bits 7:0); the only part that increments."""
221
+ return self.value & 0xFF
222
+
223
+ @property
224
+ def stepping(self) -> int:
225
+ """CPU stepping (bits 11:8)."""
226
+ return (self.value >> 8) & 0xF
227
+
228
+ @property
229
+ def modelbase(self) -> int:
230
+ """Model low nibble (bits 15:12)."""
231
+ return (self.value >> 12) & 0xF
232
+
233
+ @property
234
+ def reserved(self) -> int:
235
+ """Reserved field (bits 19:16); expected to be 0."""
236
+ return (self.value >> 16) & 0xF
237
+
238
+ @property
239
+ def modelext(self) -> int:
240
+ """Model high nibble (bits 23:20)."""
241
+ return (self.value >> 20) & 0xF
242
+
243
+ @property
244
+ def familyext(self) -> int:
245
+ """Extended family (bits 31:24); ``family == 0xF + familyext``."""
246
+ return (self.value >> 24) & 0xFF
247
+
248
+ @property
249
+ def family(self) -> int:
250
+ """Effective family (``0xF + familyext``)."""
251
+ return 0xF + self.familyext
252
+
253
+ @property
254
+ def model(self) -> int:
255
+ """Effective model (``(modelext << 4) | modelbase``)."""
256
+ return (self.modelext << 4) | self.modelbase
257
+
258
+ def matches(self, cpuid) -> bool:
259
+ """Whether the embedded f/m/s match an ``AmdCpuId`` (upper 3 bytes)."""
260
+ expected = (
261
+ (cpuid.familyext << 16)
262
+ | (cpuid.modelext << 12)
263
+ | (cpuid.modelbase << 4)
264
+ | cpuid.stepping
265
+ )
266
+ return (self.value >> 8) == expected
267
+
268
+ def is_valid(self, cpuid=None) -> bool:
269
+ if self.reserved != 0:
270
+ return False
271
+ if self.familyext < (V2_MIN_FAMILY - 0xF):
272
+ return False
273
+ if cpuid is not None and not self.matches(cpuid):
274
+ return False
275
+ return True
276
+
277
+ def _cmp_same(self, other: "PatchLevel") -> int:
278
+ if (self.value >> 8) != (other.value >> 8):
279
+ raise PatchLevelMismatchError(f"patch levels target different CPUs: {self} vs {other}")
280
+ return (self.rev > other.rev) - (self.rev < other.rev)
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ from dataclasses import dataclass
5
+ from Crypto.Cipher import AES
6
+ from Crypto.Hash import CMAC
7
+ from amd_ucode_patch.utils.rsa import digest_recover, pkcs1_v15_verify
8
+
9
+
10
+ @dataclass
11
+ class Signature:
12
+ """
13
+ Cryptographic signature block of a Zen (family >= 0x17) AMD microcode patch.
14
+
15
+ It sits immediately after the 32-byte header core and is absent on pre-Zen
16
+ patches. Layout (offsets are from the start of the patch)::
17
+
18
+ offset 32 signature[256] RSA-2048 signature (PKCS#1 v1.5, e=0x10001)
19
+ offset 288 modulus[256] RSA-2048 public key, embedded in the patch
20
+ offset 544 check[256] Montgomery helper derived from the modulus
21
+ """
22
+
23
+ SIG_SIZE = 256
24
+ MODULUS_SIZE = 256
25
+ CHECK_SIZE = 256
26
+ SIZE = SIG_SIZE + MODULUS_SIZE + CHECK_SIZE
27
+
28
+ signature: bytes
29
+ modulus: bytes
30
+ check: bytes
31
+
32
+ @staticmethod
33
+ def from_bytes(buf: bytes) -> "Signature":
34
+ if len(buf) < Signature.SIZE:
35
+ raise ValueError("not enough bytes for AMD signature block")
36
+ return Signature(
37
+ signature=buf[0:Signature.SIG_SIZE],
38
+ modulus=buf[Signature.SIG_SIZE:Signature.SIG_SIZE + Signature.MODULUS_SIZE],
39
+ check=buf[Signature.SIG_SIZE + Signature.MODULUS_SIZE:Signature.SIZE],
40
+ )
41
+
42
+ def to_bytes(self) -> bytes:
43
+ return self.signature + self.modulus + self.check
44
+
45
+ def recover_digest(self) -> bytes | None:
46
+ """
47
+ Recover the digest this signature commits to, using only the embedded
48
+ public ``modulus`` (no CMAC key required): compute
49
+ ``signature ^ 0x10001 mod modulus`` and strip the PKCS#1 v1.5 padding.
50
+
51
+ Returns the recovered payload — the 16-byte AES-CMAC AMD signed — or
52
+ ``None`` if the recovered block is not well-formed PKCS#1 v1.5 (e.g. a
53
+ corrupt signature or wrong modulus).
54
+ """
55
+ return digest_recover(self.signature, self.modulus)
56
+
57
+ def verify(self, signed_region: bytes, cmac_key: bytes) -> bool:
58
+ """
59
+ Return ``True`` if this RSA signature, checked against the embedded
60
+ ``modulus``, recovers the AES-CMAC of ``signed_region`` (the body:
61
+ ``options`` + ``rev`` + match registers + opquads).
62
+
63
+ Uses the Zen 1-4 CMAC key by default; Zen 5 uses a different, unpublished
64
+ key, so Zen 5 patches will not verify unless the right ``cmac_key`` is
65
+ supplied.
66
+ """
67
+ digest = CMAC.new(cmac_key, msg=signed_region, ciphermod=AES).digest()
68
+ return pkcs1_v15_verify(self.signature, self.modulus, digest)
@@ -3,32 +3,33 @@
3
3
 
4
4
  import struct
5
5
  from dataclasses import dataclass
6
-
6
+ from amd_ucode_patch.structures.patch_level import PatchLevel
7
7
 
8
8
 
9
9
  @dataclass
10
10
  class VerifiedHeader:
11
- FMT = "<BB BB I"
12
- SIZE = struct.calcsize(FMT)
11
+ # Fields before the trailing PatchLevel.
12
+ FMT = "<BB BB"
13
+ SIZE = struct.calcsize(FMT) + PatchLevel.SIZE
13
14
 
14
15
  autorun: int
15
16
  encrypted: int
16
17
  unk0: int
17
18
  unk1: int
18
- update_revision: int
19
+ patch_level: PatchLevel
19
20
 
20
21
  @staticmethod
21
- def from_bytes(buf: bytes) -> "VerifiedHeader":
22
+ def from_bytes(buf: bytes, family: int | None = None) -> "VerifiedHeader":
22
23
  if len(buf) < VerifiedHeader.SIZE:
23
24
  raise ValueError("not enough bytes for AMD verified header")
24
- chunk = buf[0:VerifiedHeader.SIZE]
25
- vals = struct.unpack(VerifiedHeader.FMT, chunk)
25
+ head = struct.calcsize(VerifiedHeader.FMT)
26
+ vals = struct.unpack(VerifiedHeader.FMT, buf[0:head])
26
27
  return VerifiedHeader(
27
28
  autorun=vals[0],
28
29
  encrypted=vals[1],
29
30
  unk0=vals[2],
30
31
  unk1=vals[3],
31
- update_revision=vals[4],
32
+ patch_level=PatchLevel.from_bytes(buf[head:VerifiedHeader.SIZE], family=family),
32
33
  )
33
34
 
34
35
  def to_bytes(self) -> bytes:
@@ -38,5 +39,4 @@ class VerifiedHeader:
38
39
  self.encrypted,
39
40
  self.unk0,
40
41
  self.unk1,
41
- self.update_revision,
42
- )
42
+ ) + self.patch_level.to_bytes()
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ """
5
+ Helpers for working with packed binary-coded-decimal (BCD) values, where each
6
+ nibble holds a single decimal digit (0-9). AMD microcode patch headers encode
7
+ their build date this way.
8
+ """
9
+
10
+
11
+ def is_bcd(value: int, num_nibbles: int) -> bool:
12
+ """
13
+ Return ``True`` if the low ``num_nibbles`` nibbles of ``value`` are all valid
14
+ decimal digits (0-9), i.e. the value is a well-formed packed-BCD number.
15
+ """
16
+ for i in range(num_nibbles):
17
+ if ((value >> (i * 4)) & 0xF) > 9:
18
+ return False
19
+ return True
20
+
21
+
22
+ def decode_bcd(value: int, num_nibbles: int) -> int:
23
+ """
24
+ Decode the low ``num_nibbles`` nibbles of a packed-BCD ``value`` into its
25
+ decimal integer equivalent (e.g. ``0x2018 -> 2018``). Callers must first
26
+ confirm the value is valid BCD via :func:`is_bcd`.
27
+ """
28
+ result = 0
29
+ multiplier = 1
30
+ for i in range(num_nibbles):
31
+ result += ((value >> (i * 4)) & 0xF) * multiplier
32
+ multiplier *= 10
33
+ return result
34
+
35
+
36
+ def encode_bcd(value: int, num_nibbles: int) -> int:
37
+ """
38
+ Encode a decimal integer ``value`` into its packed-BCD equivalent across
39
+ ``num_nibbles`` nibbles (e.g. ``2018 -> 0x2018``). Raises ``ValueError`` if
40
+ ``value`` does not fit in ``num_nibbles`` decimal digits.
41
+ """
42
+ if value < 0 or value >= 10 ** num_nibbles:
43
+ raise ValueError(f"{value} does not fit in {num_nibbles} BCD nibbles")
44
+ result = 0
45
+ for i in range(num_nibbles):
46
+ result |= (value % 10) << (i * 4)
47
+ value //= 10
48
+ return result
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ """
5
+ Minimal RSA signature verification helpers (textbook RSA + PKCS#1 v1.5 type 1).
6
+
7
+ No private-key operations and no external dependencies: verification is just a
8
+ modular exponentiation (``pow``) plus unpadding, which is all that is needed to
9
+ check an AMD microcode patch signature against its embedded public modulus.
10
+ """
11
+
12
+
13
+ def rsa_recover(signature: bytes, modulus: bytes, exponent: int = 0x10001) -> bytes:
14
+ """
15
+ Return the RSA-recovered block ``signature ** exponent mod modulus`` as a
16
+ big-endian byte string the same width as ``modulus``.
17
+ """
18
+ n = int.from_bytes(modulus, "big")
19
+ s = int.from_bytes(signature, "big")
20
+ if n == 0 or s >= n:
21
+ raise ValueError("signature is not a residue modulo the public modulus")
22
+ m = pow(s, exponent, n)
23
+ return m.to_bytes(len(modulus), "big")
24
+
25
+
26
+ def pkcs1_v15_unpad(block: bytes) -> bytes | None:
27
+ """
28
+ Strip PKCS#1 v1.5 block-type-1 padding (``00 01 FF..FF 00 || payload``) and
29
+ return the payload, or ``None`` if the padding is malformed.
30
+ """
31
+ if len(block) < 11 or block[0] != 0x00 or block[1] != 0x01:
32
+ return None
33
+ i = 2
34
+ while i < len(block) and block[i] == 0xFF:
35
+ i += 1
36
+ if i < 10 or i >= len(block) or block[i] != 0x00:
37
+ return None
38
+ return block[i + 1:]
39
+
40
+
41
+ def digest_recover(signature: bytes, modulus: bytes, exponent: int = 0x10001) -> bytes | None:
42
+ """
43
+ Recover the digest this signature commits to, using only the embedded
44
+ public ``modulus`` (no CMAC key required): compute
45
+ ``signature ^ 0x10001 mod modulus`` and strip the PKCS#1 v1.5 padding.
46
+
47
+ Returns the recovered payload — the 16-byte AES-CMAC AMD signed — or
48
+ ``None`` if the recovered block is not well-formed PKCS#1 v1.5 (e.g. a
49
+ corrupt signature or wrong modulus).
50
+ """
51
+ try:
52
+ block = rsa_recover(signature, modulus, exponent)
53
+ except ValueError:
54
+ return None
55
+ return pkcs1_v15_unpad(block)
56
+
57
+
58
+ def pkcs1_v15_verify(signature: bytes, modulus: bytes, digest: bytes,
59
+ exponent: int = 0x10001) -> bool:
60
+ """
61
+ Verify a PKCS#1 v1.5 signature whose padded payload is the raw ``digest``
62
+ (no DigestInfo ASN.1 wrapper, as AMD uses a bare 16-byte CMAC).
63
+ """
64
+ payload = digest_recover(signature, modulus, exponent)
65
+ return payload is not None and payload == digest
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env python
2
- # SPDX-License-Identifier: GPL-2.0-or-later
3
-
4
- from .structures.ucode_patch import UcodePatch
5
-
6
- def ucode_patch_name(patch: UcodePatch, enc_override=None) -> str:
7
- # Format should be:
8
- # family<family>_cpuid<cpuid>_rev<revision>_date<yyyymmdd>_enc<ee>.bin
9
- # The _enc<ee> suffix is only present when a verified header exists.
10
- name = f"family{patch.header.cpu_family:02x}_cpuid{patch.header.cpuid_str}_rev{patch.header.update_revision:08x}_date{patch.header.year:04}{patch.header.month:02}{patch.header.day:02}"
11
- if patch.verified_header is not None:
12
- enc = patch.verified_header.encrypted if enc_override is None else enc_override
13
- name += f"_enc{enc:02}"
14
- name += ".bin"
15
- return name
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python
2
- # SPDX-License-Identifier: GPL-2.0-or-later
3
-
4
- from pathlib import Path
5
- from .structures.ucode_patch import UcodePatch
6
-
7
- def ucode_patch_parse(file: Path) -> UcodePatch:
8
- with file.open("rb") as f:
9
- return UcodePatch.from_bytes(f.read())
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env python
2
- # SPDX-License-Identifier: GPL-2.0-or-later
3
-
4
- from dataclasses import dataclass
5
- from .ucode_patch_header import UcodePatchHeader
6
- from .verified_header import VerifiedHeader
7
-
8
-
9
- @dataclass
10
- class UcodePatch:
11
- header: UcodePatchHeader
12
- signature: bytes
13
- public_key: bytes
14
- verified_header: VerifiedHeader | None
15
- body: bytes
16
-
17
-
18
- @staticmethod
19
- def from_bytes(buf: bytes) -> "UcodePatch":
20
- header = UcodePatchHeader.from_bytes(buf)
21
- verified_header = None if header.cpu_family < 0x16 else VerifiedHeader.from_bytes(buf[800::])
22
- body_start_pos = UcodePatchHeader.SIZE + 256 + 512 + (0 if verified_header is None else VerifiedHeader.SIZE)
23
- return UcodePatch(
24
- header=header,
25
- signature=buf[UcodePatchHeader.SIZE:UcodePatchHeader.SIZE+256],
26
- public_key=buf[UcodePatchHeader.SIZE+256:UcodePatchHeader.SIZE+256+512],
27
- verified_header=verified_header,
28
- body=buf[body_start_pos::]
29
- )
@@ -1,126 +0,0 @@
1
- #!/usr/bin/env python
2
- # SPDX-License-Identifier: GPL-2.0-or-later
3
-
4
- import struct
5
- from dataclasses import dataclass
6
-
7
- def _weird_hex_as_dec(x: int) -> int:
8
- return int(f"{x:x}", 10)
9
-
10
-
11
- def _dec_as_weird_hex(x: int) -> int:
12
- return int(str(x), 16)
13
-
14
-
15
- @dataclass
16
- class UcodePatchHeader:
17
- '''
18
- https://github.com/torvalds/linux/blob/3544d5ce36f403db6e5c994f526101c870ffe9fe/arch/x86/kernel/cpu/microcode/amd.c#L70
19
-
20
- Original structure:
21
- struct microcode_header_amd {
22
- u32 data_code;
23
- u32 patch_id;
24
- u16 mc_patch_data_id;
25
- u8 mc_patch_data_len;
26
- u8 init_flag;
27
- u32 mc_patch_data_checksum;
28
- u32 nb_dev_id;
29
- u32 sb_dev_id;
30
- u16 processor_rev_id;
31
- u8 nb_rev_id;
32
- u8 sb_rev_id;
33
- u8 bios_api_rev;
34
- u8 reserved1[3];
35
- u32 match_reg[8];
36
- } __packed;
37
-
38
- data_code is in reality a date. We split that into u16, u8, u8.
39
- patch_id is referred to as revision in many other places.
40
- '''
41
- FMT = "<HBB I HBB I II H BBBBBB"
42
- SIZE = struct.calcsize(FMT)
43
-
44
- year: int
45
- day: int
46
- month: int
47
- update_revision: int
48
- loader_id: int
49
- unk0: int
50
- unk1: int
51
- unk2: int
52
- unk3: int
53
- unk4: int
54
- processor_rev_id: int
55
- unk5: int
56
- unk6: int
57
- unk7: int
58
- unk8: int
59
- unk9: int
60
- unk10: int
61
-
62
- @property
63
- def cpu_family(self) -> int:
64
- return 0xf + (self.processor_rev_id >> 12)
65
-
66
- @property
67
- def cpu_model(self) -> int:
68
- return (self.processor_rev_id >> 4) & 0xf
69
-
70
- @property
71
- def cpu_stepping(self) -> int:
72
- return self.processor_rev_id & 0xf
73
-
74
- @property
75
- def cpuid_str(self) -> str:
76
- hi = (self.processor_rev_id >> 8) & 0xFF
77
- lo = self.processor_rev_id & 0xFF
78
- return f"00{hi:02X}0F{lo:02X}"
79
-
80
- @staticmethod
81
- def from_bytes(buf: bytes) -> "UcodePatchHeader":
82
- if len(buf) < UcodePatchHeader.SIZE:
83
- raise ValueError("not enough bytes for AMD header")
84
- chunk = buf[0:UcodePatchHeader.SIZE]
85
- vals = struct.unpack(UcodePatchHeader.FMT, chunk)
86
- return UcodePatchHeader(
87
- year=_weird_hex_as_dec(vals[0]),
88
- day=_weird_hex_as_dec(vals[1]),
89
- month=_weird_hex_as_dec(vals[2]),
90
- update_revision=vals[3],
91
- loader_id=vals[4],
92
- unk0=vals[5],
93
- unk1=vals[6],
94
- unk2=vals[7],
95
- unk3=vals[8],
96
- unk4=vals[9],
97
- processor_rev_id=vals[10],
98
- unk5=vals[11],
99
- unk6=vals[12],
100
- unk7=vals[13],
101
- unk8=vals[14],
102
- unk9=vals[15],
103
- unk10=vals[16],
104
- )
105
-
106
- def to_bytes(self) -> bytes:
107
- return struct.pack(
108
- UcodePatchHeader.FMT,
109
- _dec_as_weird_hex(self.year),
110
- _dec_as_weird_hex(self.day),
111
- _dec_as_weird_hex(self.month),
112
- self.update_revision,
113
- self.loader_id,
114
- self.unk0,
115
- self.unk1,
116
- self.unk2,
117
- self.unk3,
118
- self.unk4,
119
- self.processor_rev_id,
120
- self.unk5,
121
- self.unk6,
122
- self.unk7,
123
- self.unk8,
124
- self.unk9,
125
- self.unk10,
126
- )