amd-ucode-patch 1.0.1__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.0.1 → amd_ucode_patch-2.0.0}/.github/workflows/build.yml +3 -3
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/.github/workflows/codeql.yml +3 -3
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/.github/workflows/scorecard.yml +3 -3
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/.gitignore +1 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/PKG-INFO +15 -26
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/README.md +12 -23
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/pyproject.toml +6 -2
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/argtypes.py +48 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/info.py +192 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/paths.py +23 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli/sign.py +156 -0
- 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/src/amd_ucode_patch/utils/cmac.py +40 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/utils/entrysign.py +133 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/utils/numtheory.py +20 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/utils/rsa.py +107 -0
- amd_ucode_patch-2.0.0/src/amd_ucode_patch/utils/xor.py +19 -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.0.1/src/amd_ucode_patch/cli/info.py +0 -92
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/cli/sign.py +0 -89
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/naming.py +0 -16
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/parse.py +0 -9
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/body.py +0 -45
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch.py +0 -36
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_date.py +0 -90
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_header.py +0 -121
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/patch_level.py +0 -280
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/signature.py +0 -68
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/structures/verified_header.py +0 -42
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/utils/rsa.py +0 -65
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/.github/dependabot.yml +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/LICENSE.md +0 -0
- {amd_ucode_patch-1.0.1/src/amd_ucode_patch → amd_ucode_patch-2.0.0/src/amd_ucode_patch/cli}/banner.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-2.0.0}/src/amd_ucode_patch/utils/bcd.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,48 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Argparse ``type=`` converters for hex-encoded key material shared by the
|
|
6
|
+
command line tools. Each raises ``argparse.ArgumentTypeError`` on malformed or
|
|
7
|
+
wrong-length input, which argparse renders as a usage error.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_fixed_hex(value: str, nbytes: int, what: str) -> bytes:
|
|
14
|
+
"""
|
|
15
|
+
Parse ``value`` as hex (an optional ``0x`` prefix is allowed) and require it
|
|
16
|
+
to decode to exactly ``nbytes`` bytes. ``what`` names the field in error
|
|
17
|
+
messages.
|
|
18
|
+
"""
|
|
19
|
+
text = value.strip().lower()
|
|
20
|
+
if text.startswith("0x"):
|
|
21
|
+
text = text[2:]
|
|
22
|
+
try:
|
|
23
|
+
data = bytes.fromhex(text)
|
|
24
|
+
except ValueError:
|
|
25
|
+
raise argparse.ArgumentTypeError(f"{what} must be hexadecimal, got {value!r}")
|
|
26
|
+
if len(data) != nbytes:
|
|
27
|
+
raise argparse.ArgumentTypeError(f"{what} must be {nbytes} bytes ({2 * nbytes} hex chars), got {len(data)}")
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_key(value: str) -> bytes:
|
|
32
|
+
"""Parse a 16-byte AES-128 CMAC key from hex."""
|
|
33
|
+
return parse_fixed_hex(value, 16, "key")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_modulus(value: str) -> bytes:
|
|
37
|
+
"""Parse a 256-byte RSA modulus from hex."""
|
|
38
|
+
return parse_fixed_hex(value, 256, "modulus")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_private(value: str) -> bytes:
|
|
42
|
+
"""Parse a 256-byte RSA private exponent from hex."""
|
|
43
|
+
return parse_fixed_hex(value, 256, "private exponent")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_check(value: str) -> bytes:
|
|
47
|
+
"""Parse a 256-byte signature check (Montgomery helper) field from hex."""
|
|
48
|
+
return parse_fixed_hex(value, 256, "check")
|
|
@@ -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()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""Shared path handling for the command line tools."""
|
|
5
|
+
|
|
6
|
+
import glob
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from collections.abc import Iterable, Iterator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def expand_paths(patterns: Iterable[str]) -> Iterator[Path]:
|
|
12
|
+
"""
|
|
13
|
+
Expand each glob pattern into the matching paths (sorted, recursive ``**``
|
|
14
|
+
supported). A pattern with no matches is yielded verbatim as a ``Path`` so the
|
|
15
|
+
caller can surface a sensible "file not found" error.
|
|
16
|
+
"""
|
|
17
|
+
for pattern in patterns:
|
|
18
|
+
matches = sorted(glob.glob(pattern, recursive=True))
|
|
19
|
+
if not matches:
|
|
20
|
+
yield Path(pattern)
|
|
21
|
+
continue
|
|
22
|
+
for match in matches:
|
|
23
|
+
yield Path(match)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
A command line tool to verify and resign AMD uCode patch signatures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from rich import box
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from amd_ucode_patch.cli.argtypes import parse_key, parse_modulus, parse_private
|
|
17
|
+
from amd_ucode_patch.cli.banner import BANNER
|
|
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
|
|
21
|
+
from amd_ucode_patch.utils.cmac import cmac_digest
|
|
22
|
+
from amd_ucode_patch.utils.entrysign import produce_colliding_key
|
|
23
|
+
from amd_ucode_patch.utils.rsa import montgomery_n_prime, sign_pkcs1_v15_payload
|
|
24
|
+
|
|
25
|
+
COLS = ["File", "Signed", "Digest", "Valid"]
|
|
26
|
+
|
|
27
|
+
|
|
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())
|
|
31
|
+
signature = patch.header.signature
|
|
32
|
+
|
|
33
|
+
if signature is None:
|
|
34
|
+
return (str(path), "[yellow]no[/yellow]", "", "[dim]n/a[/dim]")
|
|
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
|
+
|
|
42
|
+
recovered = signature.recover_digest()
|
|
43
|
+
digest = recovered.hex() if recovered is not None else "[red]<bad padding>[/red]"
|
|
44
|
+
|
|
45
|
+
if key is None:
|
|
46
|
+
return (str(path), "[green]yes[/green]", digest, "[dim]not checked[/dim]")
|
|
47
|
+
|
|
48
|
+
verified = patch.signature_verifies(key)
|
|
49
|
+
valid = "[green]yes[/green]" if verified else "[red]no[/red]"
|
|
50
|
+
return (str(path), "[green]yes[/green]", digest, valid)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify(args, console: Console) -> None:
|
|
54
|
+
"""The ``verify`` command: print a signature table for each file."""
|
|
55
|
+
table = Table(*COLS, box=box.HEAVY_HEAD)
|
|
56
|
+
for path in expand_paths(args.files):
|
|
57
|
+
try:
|
|
58
|
+
table.add_row(*_verify_row(path, args.key))
|
|
59
|
+
except Exception as exc: # noqa: BLE001
|
|
60
|
+
console.log(f"Error reading {path}: {exc}")
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resign_one(
|
|
65
|
+
path: Path,
|
|
66
|
+
key: bytes,
|
|
67
|
+
private: bytes | None,
|
|
68
|
+
modulus: bytes | None,
|
|
69
|
+
output: Path | None,
|
|
70
|
+
) -> tuple[Path, str]:
|
|
71
|
+
"""Resign one patch, returning where it was written and the signed digest."""
|
|
72
|
+
patch = Patch.from_bytes(path.read_bytes())
|
|
73
|
+
signature = patch.header.signature
|
|
74
|
+
if not isinstance(signature, SignatureFam17Plus):
|
|
75
|
+
raise ValueError("patch has no RSA signature to resign")
|
|
76
|
+
|
|
77
|
+
digest = cmac_digest(patch.body.to_bytes(), key)
|
|
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.
|
|
82
|
+
target = cmac_digest(signature.modulus, key)
|
|
83
|
+
new_modulus, new_private = produce_colliding_key(target, cmac_key=key)
|
|
84
|
+
else:
|
|
85
|
+
new_modulus = modulus if modulus is not None else signature.modulus
|
|
86
|
+
new_private = private
|
|
87
|
+
|
|
88
|
+
signature.modulus = new_modulus
|
|
89
|
+
signature.check = montgomery_n_prime(new_modulus)
|
|
90
|
+
signature.signature = sign_pkcs1_v15_payload(digest, new_modulus, new_private)
|
|
91
|
+
|
|
92
|
+
out_path = output if output is not None else path
|
|
93
|
+
out_path.write_bytes(patch.to_bytes())
|
|
94
|
+
return out_path, digest.hex()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resign(args, console: Console) -> int:
|
|
98
|
+
"""
|
|
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.
|
|
103
|
+
"""
|
|
104
|
+
if args.output is not None and len(args.files) != 1:
|
|
105
|
+
console.log("Error: --output is only allowed with a single input file")
|
|
106
|
+
return 1
|
|
107
|
+
if args.modulus is not None and args.private is None:
|
|
108
|
+
console.log("Error: --modulus requires --private")
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
exit_code = 0
|
|
112
|
+
for path in expand_paths(args.files):
|
|
113
|
+
try:
|
|
114
|
+
target, digest = _resign_one(
|
|
115
|
+
path, args.key, args.private, args.modulus, args.output
|
|
116
|
+
)
|
|
117
|
+
console.print(f"[green]resigned[/green] {target} digest={digest}")
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
console.log(f"Error resigning {path}: {exc}")
|
|
120
|
+
exit_code = 1
|
|
121
|
+
return exit_code
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
console = Console()
|
|
126
|
+
console.print(BANNER, highlight=False)
|
|
127
|
+
|
|
128
|
+
parser = argparse.ArgumentParser(
|
|
129
|
+
description="Inspect, verify, sign and resign AMD microcode patch signatures.",
|
|
130
|
+
epilog="The published Zen 1-4 CMAC key is 2b7e151628aed2a6abf7158809cf4f3c.",
|
|
131
|
+
)
|
|
132
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
133
|
+
|
|
134
|
+
verify_parser = subparsers.add_parser("verify", help="Inspect signatures, and verify against the body with -k")
|
|
135
|
+
verify_parser.add_argument("files", nargs="+", help="Patch files to inspect")
|
|
136
|
+
verify_parser.add_argument("-k", "--key", type=parse_key, default=None, help="AES-128 CMAC key as 32 hex chars. Enables body verification.")
|
|
137
|
+
|
|
138
|
+
resign_parser = subparsers.add_parser("resign", help="Re-sign edited patches in-place (zentool-style)")
|
|
139
|
+
resign_parser.add_argument("files", nargs="+", help="Patch files to resign")
|
|
140
|
+
resign_parser.add_argument("-k", "--key", type=parse_key, required=True, help="AES-128 CMAC key as 32 hex chars")
|
|
141
|
+
resign_parser.add_argument("-d", "--private", type=parse_private, default=None, help="RSA private exponent as 512 hex chars (256 bytes). Optional.")
|
|
142
|
+
resign_parser.add_argument("-m", "--modulus", type=parse_modulus, default=None, help="RSA modulus as 512 hex chars (256 bytes). Used with --private.")
|
|
143
|
+
resign_parser.add_argument("-o", "--output", type=Path, default=None, help="Output file (single-input only). Defaults to in-place rewrite")
|
|
144
|
+
|
|
145
|
+
args = parser.parse_args()
|
|
146
|
+
|
|
147
|
+
if args.command == "verify":
|
|
148
|
+
verify(args, console)
|
|
149
|
+
elif args.command == "resign":
|
|
150
|
+
sys.exit(resign(args, console))
|
|
151
|
+
else:
|
|
152
|
+
parser.print_help()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
main()
|