amd-ucode-patch 1.1.0__tar.gz → 2.0.0__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 (64) hide show
  1. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/build.yml +3 -3
  2. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/codeql.yml +3 -3
  3. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/scorecard.yml +3 -3
  4. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.gitignore +1 -0
  5. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/PKG-INFO +15 -26
  6. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/README.md +12 -23
  7. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/pyproject.toml +6 -2
  8. amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/info.py +192 -0
  9. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/sign.py +46 -43
  10. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body.py +61 -0
  11. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data.py +21 -0
  12. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_encrypted.py +26 -0
  13. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_plaintext.py +26 -0
  14. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_registry.py +22 -0
  15. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_header.py +84 -0
  16. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/date.py +64 -0
  17. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header.py +97 -0
  18. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data.py +29 -0
  19. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_default.py +66 -0
  20. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_fam0fto12.py +99 -0
  21. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_registry.py +43 -0
  22. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/loader_id.py +46 -0
  23. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/patch.py +99 -0
  24. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/patch_level.py +137 -0
  25. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature.py +27 -0
  26. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_fam16.py +38 -0
  27. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_fam17plus.py +86 -0
  28. amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_registry.py +36 -0
  29. amd_ucode_patch-2.0.0/tests/baseline_facts.py +54 -0
  30. amd_ucode_patch-2.0.0/tests/conftest.py +111 -0
  31. amd_ucode_patch-2.0.0/tests/data/baseline.json +3442 -0
  32. amd_ucode_patch-2.0.0/tests/test_baseline.py +34 -0
  33. amd_ucode_patch-2.0.0/tests/test_body.py +159 -0
  34. amd_ucode_patch-2.0.0/tests/test_date.py +58 -0
  35. amd_ucode_patch-2.0.0/tests/test_header.py +148 -0
  36. amd_ucode_patch-2.0.0/tests/test_header_data.py +276 -0
  37. amd_ucode_patch-2.0.0/tests/test_loader_id.py +45 -0
  38. amd_ucode_patch-2.0.0/tests/test_naming.py +118 -0
  39. amd_ucode_patch-2.0.0/tests/test_patch.py +90 -0
  40. amd_ucode_patch-2.0.0/tests/test_patch_level.py +184 -0
  41. amd_ucode_patch-2.0.0/tests/test_roundtrip.py +24 -0
  42. amd_ucode_patch-2.0.0/tests/test_signature.py +93 -0
  43. amd_ucode_patch-2.0.0/tests/test_signature_verify.py +130 -0
  44. amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/info.py +0 -81
  45. amd_ucode_patch-1.1.0/src/amd_ucode_patch/naming.py +0 -18
  46. amd_ucode_patch-1.1.0/src/amd_ucode_patch/parse.py +0 -9
  47. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/body.py +0 -45
  48. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch.py +0 -50
  49. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_date.py +0 -90
  50. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_header.py +0 -121
  51. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_level.py +0 -280
  52. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/signature.py +0 -69
  53. amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/verified_header.py +0 -42
  54. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/dependabot.yml +0 -0
  55. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/LICENSE.md +0 -0
  56. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/argtypes.py +0 -0
  57. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/banner.py +0 -0
  58. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/paths.py +0 -0
  59. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/bcd.py +0 -0
  60. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/cmac.py +0 -0
  61. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/entrysign.py +0 -0
  62. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/numtheory.py +0 -0
  63. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/rsa.py +0 -0
  64. {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/xor.py +0 -0
@@ -18,10 +18,10 @@ jobs:
18
18
 
19
19
  steps:
20
20
  - name: Checkout repository
21
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
21
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
22
22
 
23
23
  - name: Set up Python ${{ matrix.python-version }}
24
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
24
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
 
@@ -58,4 +58,4 @@ jobs:
58
58
  path: dist/
59
59
 
60
60
  - name: Publish distribution to PyPI
61
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
61
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
@@ -48,7 +48,7 @@ jobs:
48
48
  # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
49
49
  steps:
50
50
  - name: Checkout repository
51
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
51
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
52
52
 
53
53
  # Add any setup steps before running the `github/codeql-action/init` action.
54
54
  # This includes steps like installing compilers or runtimes (`actions/setup-node`
@@ -58,7 +58,7 @@ jobs:
58
58
 
59
59
  # Initializes the CodeQL tools for scanning.
60
60
  - name: Initialize CodeQL
61
- uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
61
+ uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
62
62
  with:
63
63
  languages: ${{ matrix.language }}
64
64
  build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
86
86
  exit 1
87
87
 
88
88
  - name: Perform CodeQL Analysis
89
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
89
+ uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
90
90
  with:
91
91
  category: "/language:${{matrix.language}}"
@@ -19,12 +19,12 @@ jobs:
19
19
 
20
20
  steps:
21
21
  - name: Checkout repository
22
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
22
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
23
  with:
24
24
  persist-credentials: false
25
25
 
26
26
  - name: Run analysis
27
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
27
+ uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
28
28
  with:
29
29
  results_file: results.sarif
30
30
  results_format: sarif
@@ -38,6 +38,6 @@ jobs:
38
38
  retention-days: 5
39
39
 
40
40
  - name: Upload to code-scanning
41
- uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
41
+ uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
42
42
  with:
43
43
  sarif_file: results.sarif
@@ -1,2 +1,3 @@
1
1
  # Python
2
2
  __pycache__/
3
+ dist/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: amd-ucode-patch
3
- Version: 1.1.0
3
+ Version: 2.0.0
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,7 @@ 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: amd-cpuid==1.0.1
26
+ Requires-Dist: amd-cpuid==1.0.2
27
27
  Requires-Dist: pycryptodome==3.23.0
28
28
  Requires-Dist: rich==15.0.0
29
29
  Description-Content-Type: text/markdown
@@ -47,35 +47,24 @@ pip install amd-ucode-patch
47
47
 
48
48
  ## Command line tools
49
49
 
50
- Installing the package provides the following command line tools.
50
+ Installing the package provides the following command line tools. Run any tool
51
+ with `--help` for usage details.
51
52
 
52
- ### `amd_ucode_patch_info`
53
-
54
- Inspect one or more AMD microcode patch files and print their header
55
- information as a table.
56
-
57
- ```bash
58
- amd_ucode_patch_info <files...> [-f {text,md,csv}]
59
- ```
60
-
61
- Arguments:
62
-
63
- - `files`: One or more patch files to inspect. Glob patterns (e.g. `*.bin` or `**/*.bin`) are expanded automatically.
64
- - `-f`, `--format`: Output format. One of:
65
- - `text` (default): A table rendered for the terminal.
66
- - `md`: A Markdown table.
67
- - `csv`: Comma separated values.
68
-
69
- For every patch file the following fields are reported: file name, date, update revision, loader ID, processor revision, CPUID, family, model, stepping, autorun, encrypted and body size.
53
+ | Tool | Description |
54
+ | ---------------------- | -------------------------------------------------------------------------------------- |
55
+ | `amd_ucode_patch_info` | Inspect patch files and print their decoded fields as a table (text, Markdown or CSV). |
56
+ | `amd_ucode_patch_sign` | Inspect, verify (`verify`) and re-sign (`resign`) patch signatures. |
70
57
 
71
58
  ## Library usage
72
59
 
73
60
  The package can also be used programmatically:
74
61
 
75
62
  ```python
76
- from amd_ucode_patch.parse import ucode_patch_parse
63
+ from pathlib import Path
64
+ from amd_ucode_patch.structures.patch import Patch
77
65
 
78
- patch = ucode_patch_parse("firmware.bin")
79
- print(patch.header.cpuid_str)
80
- print(patch.header.update_revision)
66
+ patch = Patch.from_file(Path("firmware.bin"))
67
+ print(patch.header.patch_level) # e.g. 0a000033
68
+ print(f"{patch.header.data.cpuid.cpuid_signature:08X}") # e.g. 00A00F00
69
+ print(patch.name_canonical) # canonical filename
81
70
  ```
@@ -17,35 +17,24 @@ pip install amd-ucode-patch
17
17
 
18
18
  ## Command line tools
19
19
 
20
- Installing the package provides the following command line tools.
20
+ Installing the package provides the following command line tools. Run any tool
21
+ with `--help` for usage details.
21
22
 
22
- ### `amd_ucode_patch_info`
23
-
24
- Inspect one or more AMD microcode patch files and print their header
25
- information as a table.
26
-
27
- ```bash
28
- amd_ucode_patch_info <files...> [-f {text,md,csv}]
29
- ```
30
-
31
- Arguments:
32
-
33
- - `files`: One or more patch files to inspect. Glob patterns (e.g. `*.bin` or `**/*.bin`) are expanded automatically.
34
- - `-f`, `--format`: Output format. One of:
35
- - `text` (default): A table rendered for the terminal.
36
- - `md`: A Markdown table.
37
- - `csv`: Comma separated values.
38
-
39
- For every patch file the following fields are reported: file name, date, update revision, loader ID, processor revision, CPUID, family, model, stepping, autorun, encrypted and body size.
23
+ | Tool | Description |
24
+ | ---------------------- | -------------------------------------------------------------------------------------- |
25
+ | `amd_ucode_patch_info` | Inspect patch files and print their decoded fields as a table (text, Markdown or CSV). |
26
+ | `amd_ucode_patch_sign` | Inspect, verify (`verify`) and re-sign (`resign`) patch signatures. |
40
27
 
41
28
  ## Library usage
42
29
 
43
30
  The package can also be used programmatically:
44
31
 
45
32
  ```python
46
- from amd_ucode_patch.parse import ucode_patch_parse
33
+ from pathlib import Path
34
+ from amd_ucode_patch.structures.patch import Patch
47
35
 
48
- patch = ucode_patch_parse("firmware.bin")
49
- print(patch.header.cpuid_str)
50
- print(patch.header.update_revision)
36
+ patch = Patch.from_file(Path("firmware.bin"))
37
+ print(patch.header.patch_level) # e.g. 0a000033
38
+ print(f"{patch.header.data.cpuid.cpuid_signature:08X}") # e.g. 00A00F00
39
+ print(patch.name_canonical) # canonical filename
51
40
  ```
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling==1.31.0", "hatch-vcs==0.5.0"]
2
+ requires = ["hatchling==1.32.0", "hatch-vcs==0.5.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -30,7 +30,7 @@ classifiers = [
30
30
  "Topic :: Utilities",
31
31
  ]
32
32
  dependencies = [
33
- "amd-cpuid==1.0.1",
33
+ "amd-cpuid==1.0.2",
34
34
  "pycryptodome==3.23.0",
35
35
  "rich==15.0.0",
36
36
  ]
@@ -49,3 +49,7 @@ source = "vcs"
49
49
 
50
50
  [tool.hatch.build.targets.wheel]
51
51
  packages = ["src/amd_ucode_patch"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = ["src"]
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ """
5
+ A command line tool to print information about AMD uCode patch files.
6
+
7
+ A format with no header-data model of its own still parses, with the fields
8
+ only that format defines left empty.
9
+ """
10
+
11
+ import argparse
12
+ import csv
13
+ import sys
14
+
15
+ from rich import box
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from amd_ucode_patch.cli.banner import BANNER
20
+ from amd_ucode_patch.cli.paths import expand_paths
21
+ from amd_ucode_patch.structures.header_data import HeaderData
22
+ from amd_ucode_patch.structures.patch import Patch
23
+
24
+ #: Columns follow the order of the fields in the patch: the prologue, then the
25
+ #: header data in offset order, then the signature block and the body header,
26
+ #: with derived values shown next to the field they come from.
27
+ COLS = ["File", "Date", "Patch level", "Loader ID", "Triads", "Init",
28
+ "Checksum", "CPUID", "Signed", "Encrypted", "Body PL", "Size"]
29
+
30
+ #: Shown wherever a field is not defined by the patch's format.
31
+ _NA = "-"
32
+
33
+
34
+ def _field(data: HeaderData, name: str) -> object | None:
35
+ """A header-data field, or ``None`` when this format does not model it."""
36
+ return getattr(data, name, None)
37
+
38
+
39
+ def _checksum_matches(data: HeaderData, raw: bytes) -> bool | None:
40
+ """
41
+ Whether the stored checksum matches the content it covers, or ``None`` when
42
+ this format has no checksum or the patch is too short to compute one.
43
+ """
44
+ stored = _field(data, "op_triad_checksum")
45
+ compute = getattr(data, "compute_op_triad_checksum", None)
46
+ if stored is None or compute is None:
47
+ return None
48
+ try:
49
+ return compute(raw) == stored
50
+ except ValueError:
51
+ return None
52
+
53
+
54
+ def _row_fields(path, raw: bytes) -> tuple[tuple[str, ...], dict[str, str]]:
55
+ """
56
+ One table row for ``path``, filling what the patch's format supports, plus a
57
+ map of column -> "green"/"red" for the cells whose value is cross-checked.
58
+ """
59
+ patch = Patch.from_bytes(raw)
60
+ header = patch.header
61
+ data = header.data
62
+ body_header = patch.body.body_header
63
+
64
+ triads = _field(data, "op_triad_count")
65
+ # The same "run the patch on load" signal lives in the pre-Zen header data
66
+ # (``init_flag``) and in the Zen body header; show whichever one this format
67
+ # carries.
68
+ init = _field(data, "init_flag")
69
+ if init is None and body_header is not None:
70
+ init = body_header.init_flag
71
+ checksum = _field(data, "op_triad_checksum")
72
+ cpuid = _field(data, "cpuid")
73
+ pl_family = header.patch_level.family
74
+ # ``encrypted`` and the mirrored patch level live only in the body header.
75
+ encrypted = body_header.encrypted if body_header is not None else None
76
+ body_pl = body_header.patch_level if body_header is not None else None
77
+
78
+ cells = (
79
+ str(path),
80
+ str(header.date),
81
+ f"{header.patch_level} (fam {pl_family:#04x})",
82
+ str(header.loader_id),
83
+ str(triads) if triads is not None else _NA,
84
+ ("yes" if init else "no") if init is not None else _NA,
85
+ f"{checksum:08x}" if checksum is not None else _NA,
86
+ f"{cpuid.ucode_signature:04x} ({cpuid.description})"
87
+ if cpuid is not None else _NA,
88
+ "yes" if header.signature is not None else "no",
89
+ ("yes" if encrypted else "no") if encrypted is not None else _NA,
90
+ str(body_pl) if body_pl is not None else _NA,
91
+ str(len(raw)),
92
+ )
93
+
94
+ colours: dict[str, str] = {}
95
+ # Flag the known-malformed patch levels red; every other one is well-formed.
96
+ colours["Patch level"] = (
97
+ "red" if header.patch_level.is_anomalous else "green"
98
+ )
99
+ checksum_ok = _checksum_matches(data, raw)
100
+ if checksum_ok is not None:
101
+ colours["Checksum"] = "green" if checksum_ok else "red"
102
+ # Cross-check the patch level against the header's CPUID field. Newer patch
103
+ # levels pack the whole CPUID, so the whole signature is checked; older ones
104
+ # carry only the family, so just that is.
105
+ pl_cpuid = header.patch_level.cpuid
106
+ if cpuid is not None:
107
+ if pl_cpuid is not None:
108
+ matches = pl_cpuid.ucode_signature == cpuid.ucode_signature
109
+ else:
110
+ matches = pl_family == cpuid.family
111
+ colours["CPUID"] = "green" if matches else "red"
112
+ if body_pl is not None:
113
+ # The body header's patch level should mirror the header's.
114
+ matches = body_pl.value == header.patch_level.value
115
+ colours["Body PL"] = "green" if matches else "red"
116
+ return cells, colours
117
+
118
+
119
+ def _rows(paths):
120
+ """
121
+ Build a row per readable patch, with its checksum verdict.
122
+
123
+ Diagnostics go to stderr so that piping the table or the CSV somewhere
124
+ yields only the data.
125
+ """
126
+ console = Console(stderr=True)
127
+ unmodelled = 0
128
+ for path in paths:
129
+ try:
130
+ raw = path.read_bytes()
131
+ cells, colours = _row_fields(path, raw)
132
+ except Exception as exc: # noqa: BLE001
133
+ console.log(f"Error parsing {path}: {exc}")
134
+ continue
135
+ unmodelled += cells[COLS.index("Triads")] == _NA
136
+ yield cells, colours
137
+ if unmodelled:
138
+ console.log(f"{unmodelled} file(s): that format has no header-data "
139
+ f"model of its own, so only shared fields are shown")
140
+
141
+
142
+ def _apply_colours(cells: tuple[str, ...], colours: dict[str, str]) -> list[str]:
143
+ """Wrap each cross-checked cell in its verdict colour (green/red)."""
144
+ out = list(cells)
145
+ for column, colour in colours.items():
146
+ index = COLS.index(column)
147
+ out[index] = f"[{colour}]{out[index]}[/{colour}]"
148
+ return out
149
+
150
+
151
+ def print_table(console: Console, paths, format):
152
+ table = Table(*COLS, box=format)
153
+ for cells, colours in _rows(paths):
154
+ table.add_row(*_apply_colours(cells, colours))
155
+ console.print(table)
156
+
157
+
158
+ def print_csv(paths):
159
+ """
160
+ Write the rows as CSV to stdout.
161
+
162
+ Written with :mod:`csv` rather than through the console: cell values contain
163
+ commas (a processor description, for one), and machine-readable output
164
+ should carry neither console formatting nor the checksum colouring.
165
+ """
166
+ writer = csv.writer(sys.stdout, lineterminator="\n")
167
+ writer.writerow(COLS)
168
+ writer.writerows(cells for cells, _ in _rows(paths))
169
+
170
+
171
+ def main():
172
+ console = Console()
173
+
174
+ parser = argparse.ArgumentParser(description="Inspect AMD microcode patch files.")
175
+ parser.add_argument("files", nargs="+", help="Patch files to inspect")
176
+ parser.add_argument("-f", "--format", choices=["text", "md", "csv"], default="text")
177
+ args = parser.parse_args()
178
+
179
+ if args.format != "csv":
180
+ console.print(BANNER, highlight=False)
181
+
182
+ paths = expand_paths(args.files)
183
+ if args.format == "text":
184
+ print_table(console, paths, box.HEAVY_HEAD)
185
+ elif args.format == "md":
186
+ print_table(console, paths, box.MARKDOWN)
187
+ elif args.format == "csv":
188
+ print_csv(paths)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -1,63 +1,66 @@
1
1
  #!/usr/bin/env python
2
2
  # SPDX-License-Identifier: GPL-2.0-or-later
3
- '''
4
- A command line tool to inspect, verify, sign and resign AMD uCode patches.
5
- '''
3
+
4
+ """
5
+ A command line tool to verify and resign AMD uCode patch signatures.
6
+ """
6
7
 
7
8
  import sys
8
9
  import argparse
9
10
  from pathlib import Path
11
+
10
12
  from rich import box
11
13
  from rich.console import Console
12
14
  from rich.table import Table
15
+
16
+ from amd_ucode_patch.cli.argtypes import parse_key, parse_modulus, parse_private
13
17
  from amd_ucode_patch.cli.banner import BANNER
14
- from amd_ucode_patch.parse import ucode_patch_parse
18
+ from amd_ucode_patch.cli.paths import expand_paths
19
+ from amd_ucode_patch.structures.patch import Patch
20
+ from amd_ucode_patch.structures.signature_fam17plus import SignatureFam17Plus
15
21
  from amd_ucode_patch.utils.cmac import cmac_digest
16
22
  from amd_ucode_patch.utils.entrysign import produce_colliding_key
17
23
  from amd_ucode_patch.utils.rsa import montgomery_n_prime, sign_pkcs1_v15_payload
18
- from amd_ucode_patch.cli.paths import expand_paths
19
- from amd_ucode_patch.cli.argtypes import parse_key, parse_modulus, parse_private
20
-
21
24
 
22
25
  COLS = ["File", "Signed", "Digest", "Valid"]
23
26
 
24
27
 
25
- def _verify_row(path, key):
26
- """Return the four display cells (File, Signed, Digest, Valid) for one patch."""
27
- patch = ucode_patch_parse(path)
28
+ def _verify_row(path: Path, key: bytes | None) -> tuple[str, ...]:
29
+ """The four display cells (File, Signed, Digest, Valid) for one patch."""
30
+ patch = Patch.from_bytes(path.read_bytes())
28
31
  signature = patch.header.signature
29
32
 
30
33
  if signature is None:
31
34
  return (str(path), "[yellow]no[/yellow]", "", "[dim]n/a[/dim]")
32
35
 
36
+ if not isinstance(signature, SignatureFam17Plus):
37
+ # A signature slot that is not the RSA block (family 0x16): present, but
38
+ # nothing to recover or verify.
39
+ return (str(path), "[green]yes[/green]", "[dim]not RSA[/dim]",
40
+ "[dim]n/a[/dim]")
41
+
33
42
  recovered = signature.recover_digest()
34
43
  digest = recovered.hex() if recovered is not None else "[red]<bad padding>[/red]"
35
44
 
36
45
  if key is None:
37
46
  return (str(path), "[green]yes[/green]", digest, "[dim]not checked[/dim]")
38
47
 
39
- computed = cmac_digest(patch.body.to_bytes(), key)
40
- verdict = recovered is not None and recovered == computed
41
- valid = "[green]yes[/green]" if verdict else "[red]no[/red]"
48
+ verified = patch.signature_verifies(key)
49
+ valid = "[green]yes[/green]" if verified else "[red]no[/red]"
42
50
  return (str(path), "[green]yes[/green]", digest, valid)
43
51
 
44
52
 
45
- def verify(args, console):
46
- """Process the ``verify`` command: print a signature table for each file."""
53
+ def verify(args, console: Console) -> None:
54
+ """The ``verify`` command: print a signature table for each file."""
47
55
  table = Table(*COLS, box=box.HEAVY_HEAD)
48
56
  for path in expand_paths(args.files):
49
57
  try:
50
- row = _verify_row(path, args.key)
51
- table.add_row(*row)
52
- except Exception as e:
53
- console.log(f"Error reading {path}: {e}")
58
+ table.add_row(*_verify_row(path, args.key))
59
+ except Exception as exc: # noqa: BLE001
60
+ console.log(f"Error reading {path}: {exc}")
54
61
  console.print(table)
55
62
 
56
63
 
57
- def _write_target(src: Path, output: Path | None) -> Path:
58
- return output if output is not None else src
59
-
60
-
61
64
  def _resign_one(
62
65
  path: Path,
63
66
  key: bytes,
@@ -65,13 +68,17 @@ def _resign_one(
65
68
  modulus: bytes | None,
66
69
  output: Path | None,
67
70
  ) -> tuple[Path, str]:
68
- patch = ucode_patch_parse(path)
71
+ """Resign one patch, returning where it was written and the signed digest."""
72
+ patch = Patch.from_bytes(path.read_bytes())
69
73
  signature = patch.header.signature
70
- if signature is None:
71
- raise ValueError("patch is unsigned (pre-Zen), cannot resign")
74
+ if not isinstance(signature, SignatureFam17Plus):
75
+ raise ValueError("patch has no RSA signature to resign")
72
76
 
73
77
  digest = cmac_digest(patch.body.to_bytes(), key)
74
78
  if private is None:
79
+ # No private key given.
80
+ # Forge a modulus that collides to the CMAC the
81
+ # loader expects, the way zentool does for the EntrySign exploit.
75
82
  target = cmac_digest(signature.modulus, key)
76
83
  new_modulus, new_private = produce_colliding_key(target, cmac_key=key)
77
84
  else:
@@ -82,16 +89,17 @@ def _resign_one(
82
89
  signature.check = montgomery_n_prime(new_modulus)
83
90
  signature.signature = sign_pkcs1_v15_payload(digest, new_modulus, new_private)
84
91
 
85
- out_path = _write_target(path, output)
92
+ out_path = output if output is not None else path
86
93
  out_path.write_bytes(patch.to_bytes())
87
94
  return out_path, digest.hex()
88
95
 
89
96
 
90
- def resign(args, console) -> int:
97
+ def resign(args, console: Console) -> int:
91
98
  """
92
- Process the ``resign`` command: re-sign edited patches, in place by default.
93
- Returns a process exit code (non-zero if any file failed or the arguments
94
- were invalid).
99
+ The ``resign`` command: re-sign edited patches, in place by default.
100
+
101
+ Returns a process exit code, non-zero if any file failed or the arguments
102
+ were invalid.
95
103
  """
96
104
  if args.output is not None and len(args.files) != 1:
97
105
  console.log("Error: --output is only allowed with a single input file")
@@ -104,15 +112,11 @@ def resign(args, console) -> int:
104
112
  for path in expand_paths(args.files):
105
113
  try:
106
114
  target, digest = _resign_one(
107
- path,
108
- args.key,
109
- args.private,
110
- args.modulus,
111
- args.output,
115
+ path, args.key, args.private, args.modulus, args.output
112
116
  )
113
117
  console.print(f"[green]resigned[/green] {target} digest={digest}")
114
- except Exception as e:
115
- console.log(f"Error resigning {path}: {e}")
118
+ except Exception as exc: # noqa: BLE001
119
+ console.log(f"Error resigning {path}: {exc}")
116
120
  exit_code = 1
117
121
  return exit_code
118
122
 
@@ -125,16 +129,15 @@ def main():
125
129
  description="Inspect, verify, sign and resign AMD microcode patch signatures.",
126
130
  epilog="The published Zen 1-4 CMAC key is 2b7e151628aed2a6abf7158809cf4f3c.",
127
131
  )
128
-
129
132
  subparsers = parser.add_subparsers(dest="command")
130
133
 
131
- verify_parser = subparsers.add_parser("verify", help="Inspect signatures and verify with -k when provided")
134
+ verify_parser = subparsers.add_parser("verify", help="Inspect signatures, and verify against the body with -k")
132
135
  verify_parser.add_argument("files", nargs="+", help="Patch files to inspect")
133
- verify_parser.add_argument("-k", "--key", type=parse_key, default=None, help="AES-128 CMAC key as hex (32 hex chars); enables verification")
136
+ verify_parser.add_argument("-k", "--key", type=parse_key, default=None, help="AES-128 CMAC key as 32 hex chars. Enables body verification.")
134
137
 
135
138
  resign_parser = subparsers.add_parser("resign", help="Re-sign edited patches in-place (zentool-style)")
136
139
  resign_parser.add_argument("files", nargs="+", help="Patch files to resign")
137
- resign_parser.add_argument("-k", "--key", type=parse_key, required=True, help="AES-128 CMAC key as hex (default: published Zen 1-4 key)")
140
+ resign_parser.add_argument("-k", "--key", type=parse_key, required=True, help="AES-128 CMAC key as 32 hex chars")
138
141
  resign_parser.add_argument("-d", "--private", type=parse_private, default=None, help="RSA private exponent as 512 hex chars (256 bytes). Optional.")
139
142
  resign_parser.add_argument("-m", "--modulus", type=parse_modulus, default=None, help="RSA modulus as 512 hex chars (256 bytes). Used with --private.")
140
143
  resign_parser.add_argument("-o", "--output", type=Path, default=None, help="Output file (single-input only). Defaults to in-place rewrite")
@@ -146,7 +149,7 @@ def main():
146
149
  elif args.command == "resign":
147
150
  sys.exit(resign(args, console))
148
151
  else:
149
- console.log(f"Error: unknown command {args.command!r}")
152
+ parser.print_help()
150
153
 
151
154
 
152
155
  if __name__ == "__main__":
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ """
5
+ The patch body.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from amd_ucode_patch.structures.body_data import BodyData
13
+ from amd_ucode_patch.structures.body_data_registry import body_data_from_bytes
14
+ from amd_ucode_patch.structures.body_header import BodyHeader
15
+
16
+
17
+ @dataclass
18
+ class Body:
19
+ """
20
+ The patch body: an optional :class:`BodyHeader` followed by the
21
+ :class:`BodyData`.
22
+
23
+ The body header's ``encrypted`` flag selects the kind of body data: opaque
24
+ when set, plaintext (a modelling target) otherwise.
25
+ """
26
+
27
+ #: The body header, on formats that carry one; ``None`` otherwise.
28
+ body_header: BodyHeader | None
29
+ #: The contents past the header.
30
+ body_data: BodyData
31
+
32
+ @classmethod
33
+ def from_bytes(cls, data: bytes, family: int) -> "Body":
34
+ """
35
+ Parse the body from ``data``, which is the body and nothing else, using
36
+ the CPU ``family`` to pick the layout.
37
+
38
+ :class:`BodyHeader` decides whether ``family`` carries a body header; if
39
+ it does, it is split off the front. The remainder is handed to the body
40
+ data registry, which picks the opaque or plaintext class from the
41
+ header's ``encrypted`` flag. A body with no header is plaintext.
42
+ """
43
+ data = bytes(data)
44
+ body_header = BodyHeader.from_bytes(data, family)
45
+ remainder = data[BodyHeader.SIZE:] if body_header is not None else data
46
+ encrypted = body_header is not None and bool(body_header.encrypted)
47
+ body_data = body_data_from_bytes(encrypted, remainder)
48
+ return cls(
49
+ body_header=body_header,
50
+ body_data=body_data,
51
+ )
52
+
53
+ @property
54
+ def is_encrypted(self) -> bool:
55
+ """Whether the body data is encrypted (and so cannot be modelled)."""
56
+ return self.body_data.is_encrypted
57
+
58
+ def to_bytes(self) -> bytes:
59
+ """Serialize the body back to its exact byte encoding."""
60
+ prefix = self.body_header.to_bytes() if self.body_header is not None else b""
61
+ return prefix + self.body_data.to_bytes()
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python
2
+ # SPDX-License-Identifier: GPL-2.0-or-later
3
+
4
+ """
5
+ The common interface of the body data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+
12
+
13
+ class BodyData(ABC):
14
+ """The contents of a body past its header, in whatever form the format uses."""
15
+
16
+ #: Whether the contents are encrypted (and so can only be raw bytes).
17
+ is_encrypted: bool
18
+
19
+ @abstractmethod
20
+ def to_bytes(self) -> bytes:
21
+ """Serialize the body data back to its exact byte encoding."""