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.
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/build.yml +3 -3
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/codeql.yml +3 -3
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/workflows/scorecard.yml +3 -3
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.gitignore +1 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/PKG-INFO +15 -26
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/README.md +12 -23
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/pyproject.toml +6 -2
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/info.py +192 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/sign.py +46 -43
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body.py +61 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data.py +21 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_encrypted.py +26 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_plaintext.py +26 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_data_registry.py +22 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/body_header.py +84 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/date.py +64 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header.py +97 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data.py +29 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_default.py +66 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_fam0fto12.py +99 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/header_data_registry.py +43 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/loader_id.py +46 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/patch.py +99 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/patch_level.py +137 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature.py +27 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_fam16.py +38 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_fam17plus.py +86 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/structures/signature_registry.py +36 -0
- amd_ucode_patch-2.0.0/tests/baseline_facts.py +54 -0
- amd_ucode_patch-2.0.0/tests/conftest.py +111 -0
- amd_ucode_patch-2.0.0/tests/data/baseline.json +3442 -0
- amd_ucode_patch-2.0.0/tests/test_baseline.py +34 -0
- amd_ucode_patch-2.0.0/tests/test_body.py +159 -0
- amd_ucode_patch-2.0.0/tests/test_date.py +58 -0
- amd_ucode_patch-2.0.0/tests/test_header.py +148 -0
- amd_ucode_patch-2.0.0/tests/test_header_data.py +276 -0
- amd_ucode_patch-2.0.0/tests/test_loader_id.py +45 -0
- amd_ucode_patch-2.0.0/tests/test_naming.py +118 -0
- amd_ucode_patch-2.0.0/tests/test_patch.py +90 -0
- amd_ucode_patch-2.0.0/tests/test_patch_level.py +184 -0
- amd_ucode_patch-2.0.0/tests/test_roundtrip.py +24 -0
- amd_ucode_patch-2.0.0/tests/test_signature.py +93 -0
- amd_ucode_patch-2.0.0/tests/test_signature_verify.py +130 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/info.py +0 -81
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/naming.py +0 -18
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/parse.py +0 -9
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/body.py +0 -45
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch.py +0 -50
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_date.py +0 -90
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_header.py +0 -121
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_level.py +0 -280
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/signature.py +0 -69
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/verified_header.py +0 -42
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/.github/dependabot.yml +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/LICENSE.md +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/argtypes.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/banner.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/cli/paths.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/bcd.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/cmac.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/entrysign.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/numtheory.py +0 -0
- {amd_ucode_patch-1.1.0 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/rsa.py +0 -0
- {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@
|
|
21
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
22
|
|
|
23
23
|
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
-
uses: actions/setup-python@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
41
|
+
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
42
42
|
with:
|
|
43
43
|
sarif_file: results.sarif
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: amd-ucode-patch
|
|
3
|
-
Version:
|
|
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.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from amd_ucode_patch.structures.patch import Patch
|
|
77
65
|
|
|
78
|
-
patch =
|
|
79
|
-
print(patch.header.
|
|
80
|
-
print(patch.header.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from amd_ucode_patch.structures.patch import Patch
|
|
47
35
|
|
|
48
|
-
patch =
|
|
49
|
-
print(patch.header.
|
|
50
|
-
print(patch.header.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
"""
|
|
27
|
-
patch =
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
71
|
-
raise ValueError("patch
|
|
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 =
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
115
|
-
console.log(f"Error resigning {path}: {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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."""
|