amd-ucode-patch 1.0.0__tar.gz → 1.1.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.0 → amd_ucode_patch-1.1.0}/.github/workflows/codeql.yml +2 -2
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/.github/workflows/scorecard.yml +1 -1
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/PKG-INFO +4 -2
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/pyproject.toml +6 -3
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/argtypes.py +48 -0
- amd_ucode_patch-1.0.0/src/amd_ucode_patch/cli_info.py → amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/info.py +24 -25
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/paths.py +23 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/sign.py +153 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/naming.py +18 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/parse.py +9 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/body.py +45 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch.py +50 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_date.py +90 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_header.py +121 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/patch_level.py +280 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/structures/signature.py +69 -0
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/verified_header.py +10 -10
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/bcd.py +48 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/cmac.py +40 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/entrysign.py +133 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/numtheory.py +20 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/rsa.py +107 -0
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/utils/xor.py +19 -0
- amd_ucode_patch-1.0.0/src/amd_ucode_patch/naming.py +0 -15
- amd_ucode_patch-1.0.0/src/amd_ucode_patch/parse.py +0 -9
- amd_ucode_patch-1.0.0/src/amd_ucode_patch/structures/ucode_patch.py +0 -29
- amd_ucode_patch-1.0.0/src/amd_ucode_patch/structures/ucode_patch_header.py +0 -126
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/.github/dependabot.yml +0 -0
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/.github/workflows/build.yml +0 -0
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/.gitignore +0 -0
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/LICENSE.md +0 -0
- {amd_ucode_patch-1.0.0 → amd_ucode_patch-1.1.0}/README.md +0 -0
- {amd_ucode_patch-1.0.0/src/amd_ucode_patch → amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli}/banner.py +0 -0
|
@@ -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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # 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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
|
90
90
|
with:
|
|
91
91
|
category: "/language:${{matrix.language}}"
|
|
@@ -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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
|
42
42
|
with:
|
|
43
43
|
sarif_file: results.sarif
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amd-ucode-patch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
23
23
|
Classifier: Topic :: System :: Hardware
|
|
24
24
|
Classifier: Topic :: Utilities
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: amd-cpuid==1.0.1
|
|
27
|
+
Requires-Dist: pycryptodome==3.23.0
|
|
28
|
+
Requires-Dist: rich==15.0.0
|
|
27
29
|
Description-Content-Type: text/markdown
|
|
28
30
|
|
|
29
31
|
# AMD Microcode Patch
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling", "hatch-vcs"]
|
|
2
|
+
requires = ["hatchling==1.31.0", "hatch-vcs==0.5.0"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -30,7 +30,9 @@ classifiers = [
|
|
|
30
30
|
"Topic :: Utilities",
|
|
31
31
|
]
|
|
32
32
|
dependencies = [
|
|
33
|
-
"
|
|
33
|
+
"amd-cpuid==1.0.1",
|
|
34
|
+
"pycryptodome==3.23.0",
|
|
35
|
+
"rich==15.0.0",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.urls]
|
|
@@ -39,7 +41,8 @@ Repository = "https://github.com/amd-zenith/amd-ucode-patch"
|
|
|
39
41
|
Issues = "https://github.com/amd-zenith/amd-ucode-patch/issues"
|
|
40
42
|
|
|
41
43
|
[project.scripts]
|
|
42
|
-
amd_ucode_patch_info = "amd_ucode_patch.
|
|
44
|
+
amd_ucode_patch_info = "amd_ucode_patch.cli.info:main"
|
|
45
|
+
amd_ucode_patch_sign = "amd_ucode_patch.cli.sign:main"
|
|
43
46
|
|
|
44
47
|
[tool.hatch.version]
|
|
45
48
|
source = "vcs"
|
|
@@ -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")
|
|
@@ -4,42 +4,41 @@
|
|
|
4
4
|
A command line tool to print information about AMD uCode patch files.
|
|
5
5
|
'''
|
|
6
6
|
|
|
7
|
-
import glob
|
|
8
7
|
import argparse
|
|
9
|
-
from pathlib import Path
|
|
10
8
|
from rich import box
|
|
11
9
|
from rich.console import Console
|
|
12
10
|
from rich.table import Table
|
|
13
|
-
from .banner import BANNER
|
|
14
|
-
from .
|
|
11
|
+
from amd_ucode_patch.cli.banner import BANNER
|
|
12
|
+
from amd_ucode_patch.cli.paths import expand_paths
|
|
13
|
+
from amd_ucode_patch.parse import ucode_patch_parse
|
|
14
|
+
from amd_ucode_patch.structures.patch_level import PatchLevelV2
|
|
15
15
|
|
|
16
|
-
COLS = ["File", "Date", "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def expand_paths(patterns):
|
|
20
|
-
for pattern in patterns:
|
|
21
|
-
matches = sorted(glob.glob(pattern, recursive=True))
|
|
22
|
-
if not matches:
|
|
23
|
-
yield Path(pattern)
|
|
24
|
-
continue
|
|
25
|
-
for match in matches:
|
|
26
|
-
yield Path(match)
|
|
16
|
+
COLS = ["File", "Date", "Patch level", "PL Ver", "PL Rev", "Loader ID", "Proc. Rev", "CPUID", "Family", "Family Name", "Model", "Stepping", "Microarch", "Codename", "Signed", "Autorun", "Encrypted", "Body size"]
|
|
27
17
|
|
|
28
18
|
|
|
29
19
|
def _row_fields(path, patch):
|
|
20
|
+
patch_level = patch.header.patch_level
|
|
21
|
+
# Advanced (v2 / Zen+) patch level fields are only meaningful when present.
|
|
22
|
+
patch_rev = f"{patch_level.rev:02x}" if isinstance(patch_level, PatchLevelV2) else ""
|
|
30
23
|
return (
|
|
31
24
|
str(path),
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
str(patch.header.date),
|
|
26
|
+
str(patch_level),
|
|
27
|
+
f"v{int(patch_level.version)}",
|
|
28
|
+
patch_rev,
|
|
34
29
|
f"{patch.header.loader_id:04x}",
|
|
35
|
-
f"{patch.header.
|
|
36
|
-
patch.header.
|
|
37
|
-
f"0x{patch.header.
|
|
38
|
-
|
|
39
|
-
f"0x{patch.header.
|
|
40
|
-
f"{patch.
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
f"{patch.header.cpuid.ucode_signature:04x}",
|
|
31
|
+
f"{patch.header.cpuid.cpuid_signature:08X}",
|
|
32
|
+
f"0x{patch.header.cpuid.family:02x}",
|
|
33
|
+
patch.header.cpuid.familyname,
|
|
34
|
+
f"0x{patch.header.cpuid.model:02x}",
|
|
35
|
+
f"0x{patch.header.cpuid.stepping:02x}",
|
|
36
|
+
patch.header.cpuid.microarchitecture,
|
|
37
|
+
patch.header.cpuid.codename,
|
|
38
|
+
"yes" if patch.header.signature is not None else "no",
|
|
39
|
+
f"{patch.body.verified_header.autorun if patch.body.verified_header is not None else ''}",
|
|
40
|
+
f"{patch.body.verified_header.encrypted if patch.body.verified_header is not None else ''}",
|
|
41
|
+
f"{len(patch.body.data)}",
|
|
43
42
|
)
|
|
44
43
|
|
|
45
44
|
def print_table(console: Console, paths, format):
|
|
@@ -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,153 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
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
|
+
'''
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from amd_ucode_patch.cli.banner import BANNER
|
|
14
|
+
from amd_ucode_patch.parse import ucode_patch_parse
|
|
15
|
+
from amd_ucode_patch.utils.cmac import cmac_digest
|
|
16
|
+
from amd_ucode_patch.utils.entrysign import produce_colliding_key
|
|
17
|
+
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
|
+
|
|
22
|
+
COLS = ["File", "Signed", "Digest", "Valid"]
|
|
23
|
+
|
|
24
|
+
|
|
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
|
+
signature = patch.header.signature
|
|
29
|
+
|
|
30
|
+
if signature is None:
|
|
31
|
+
return (str(path), "[yellow]no[/yellow]", "", "[dim]n/a[/dim]")
|
|
32
|
+
|
|
33
|
+
recovered = signature.recover_digest()
|
|
34
|
+
digest = recovered.hex() if recovered is not None else "[red]<bad padding>[/red]"
|
|
35
|
+
|
|
36
|
+
if key is None:
|
|
37
|
+
return (str(path), "[green]yes[/green]", digest, "[dim]not checked[/dim]")
|
|
38
|
+
|
|
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]"
|
|
42
|
+
return (str(path), "[green]yes[/green]", digest, valid)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify(args, console):
|
|
46
|
+
"""Process the ``verify`` command: print a signature table for each file."""
|
|
47
|
+
table = Table(*COLS, box=box.HEAVY_HEAD)
|
|
48
|
+
for path in expand_paths(args.files):
|
|
49
|
+
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}")
|
|
54
|
+
console.print(table)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _write_target(src: Path, output: Path | None) -> Path:
|
|
58
|
+
return output if output is not None else src
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resign_one(
|
|
62
|
+
path: Path,
|
|
63
|
+
key: bytes,
|
|
64
|
+
private: bytes | None,
|
|
65
|
+
modulus: bytes | None,
|
|
66
|
+
output: Path | None,
|
|
67
|
+
) -> tuple[Path, str]:
|
|
68
|
+
patch = ucode_patch_parse(path)
|
|
69
|
+
signature = patch.header.signature
|
|
70
|
+
if signature is None:
|
|
71
|
+
raise ValueError("patch is unsigned (pre-Zen), cannot resign")
|
|
72
|
+
|
|
73
|
+
digest = cmac_digest(patch.body.to_bytes(), key)
|
|
74
|
+
if private is None:
|
|
75
|
+
target = cmac_digest(signature.modulus, key)
|
|
76
|
+
new_modulus, new_private = produce_colliding_key(target, cmac_key=key)
|
|
77
|
+
else:
|
|
78
|
+
new_modulus = modulus if modulus is not None else signature.modulus
|
|
79
|
+
new_private = private
|
|
80
|
+
|
|
81
|
+
signature.modulus = new_modulus
|
|
82
|
+
signature.check = montgomery_n_prime(new_modulus)
|
|
83
|
+
signature.signature = sign_pkcs1_v15_payload(digest, new_modulus, new_private)
|
|
84
|
+
|
|
85
|
+
out_path = _write_target(path, output)
|
|
86
|
+
out_path.write_bytes(patch.to_bytes())
|
|
87
|
+
return out_path, digest.hex()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resign(args, console) -> int:
|
|
91
|
+
"""
|
|
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).
|
|
95
|
+
"""
|
|
96
|
+
if args.output is not None and len(args.files) != 1:
|
|
97
|
+
console.log("Error: --output is only allowed with a single input file")
|
|
98
|
+
return 1
|
|
99
|
+
if args.modulus is not None and args.private is None:
|
|
100
|
+
console.log("Error: --modulus requires --private")
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
exit_code = 0
|
|
104
|
+
for path in expand_paths(args.files):
|
|
105
|
+
try:
|
|
106
|
+
target, digest = _resign_one(
|
|
107
|
+
path,
|
|
108
|
+
args.key,
|
|
109
|
+
args.private,
|
|
110
|
+
args.modulus,
|
|
111
|
+
args.output,
|
|
112
|
+
)
|
|
113
|
+
console.print(f"[green]resigned[/green] {target} digest={digest}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
console.log(f"Error resigning {path}: {e}")
|
|
116
|
+
exit_code = 1
|
|
117
|
+
return exit_code
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
console = Console()
|
|
122
|
+
console.print(BANNER, highlight=False)
|
|
123
|
+
|
|
124
|
+
parser = argparse.ArgumentParser(
|
|
125
|
+
description="Inspect, verify, sign and resign AMD microcode patch signatures.",
|
|
126
|
+
epilog="The published Zen 1-4 CMAC key is 2b7e151628aed2a6abf7158809cf4f3c.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
130
|
+
|
|
131
|
+
verify_parser = subparsers.add_parser("verify", help="Inspect signatures and verify with -k when provided")
|
|
132
|
+
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")
|
|
134
|
+
|
|
135
|
+
resign_parser = subparsers.add_parser("resign", help="Re-sign edited patches in-place (zentool-style)")
|
|
136
|
+
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)")
|
|
138
|
+
resign_parser.add_argument("-d", "--private", type=parse_private, default=None, help="RSA private exponent as 512 hex chars (256 bytes). Optional.")
|
|
139
|
+
resign_parser.add_argument("-m", "--modulus", type=parse_modulus, default=None, help="RSA modulus as 512 hex chars (256 bytes). Used with --private.")
|
|
140
|
+
resign_parser.add_argument("-o", "--output", type=Path, default=None, help="Output file (single-input only). Defaults to in-place rewrite")
|
|
141
|
+
|
|
142
|
+
args = parser.parse_args()
|
|
143
|
+
|
|
144
|
+
if args.command == "verify":
|
|
145
|
+
verify(args, console)
|
|
146
|
+
elif args.command == "resign":
|
|
147
|
+
sys.exit(resign(args, console))
|
|
148
|
+
else:
|
|
149
|
+
console.log(f"Error: unknown command {args.command!r}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
from .structures.patch import Patch
|
|
5
|
+
|
|
6
|
+
def ucode_patch_name(patch: Patch, enc_override=None) -> str:
|
|
7
|
+
# Format should be:
|
|
8
|
+
# family<family>_cpuid<cpuid>_rev<revision>_date<yyyymmdd>_enc<ee>_sha<hash12>.bin
|
|
9
|
+
# The _enc<ee> suffix is only present when a verified header exists.
|
|
10
|
+
cpuid = patch.header.cpuid
|
|
11
|
+
name = f"family{cpuid.family:02x}_cpuid{cpuid.cpuid_signature:08X}_rev{patch.header.patch_level}_date{patch.header.date.year:04}{patch.header.date.month:02}{patch.header.date.day:02}"
|
|
12
|
+
if patch.body.verified_header is not None:
|
|
13
|
+
enc = patch.body.verified_header.encrypted if enc_override is None else enc_override
|
|
14
|
+
name += f"_enc{enc:02}"
|
|
15
|
+
hash12 = patch.sha256_hex()[:12]
|
|
16
|
+
name += f"_sha{hash12}"
|
|
17
|
+
name += ".bin"
|
|
18
|
+
return name
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from .verified_header import VerifiedHeader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
#: Family at/after which the body carries a verified header (options + rev).
|
|
9
|
+
#: This is the Zen boundary; kept independent of the signature gating.
|
|
10
|
+
VERIFIED_HEADER_MIN_FAMILY = 0x17
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Body:
|
|
15
|
+
"""
|
|
16
|
+
The signed portion of an AMD microcode patch -- the region the RSA signature
|
|
17
|
+
in :class:`PatchHeader` covers (through the AES-CMAC).
|
|
18
|
+
|
|
19
|
+
On Zen (signed) patches it is the ``options``/``rev`` block
|
|
20
|
+
(:class:`VerifiedHeader`) followed by the match registers and the (encrypted)
|
|
21
|
+
microcode opquads. On pre-Zen patches there is no verified header and the
|
|
22
|
+
whole thing is the raw, unsigned microcode.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
verified_header: VerifiedHeader | None
|
|
26
|
+
data: bytes
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def from_bytes(buf: bytes, family: int | None = None) -> "Body":
|
|
30
|
+
"""
|
|
31
|
+
Parse the body. The verified header (options + rev) is present only on
|
|
32
|
+
Zen patches (family >= ``VERIFIED_HEADER_MIN_FAMILY``); ``family`` selects
|
|
33
|
+
that. When ``family`` is unknown the body is treated as having none.
|
|
34
|
+
"""
|
|
35
|
+
if family is not None and family >= VERIFIED_HEADER_MIN_FAMILY:
|
|
36
|
+
verified_header = VerifiedHeader.from_bytes(buf, family=family)
|
|
37
|
+
data = buf[VerifiedHeader.SIZE:]
|
|
38
|
+
else:
|
|
39
|
+
verified_header = None
|
|
40
|
+
data = buf
|
|
41
|
+
return Body(verified_header=verified_header, data=data)
|
|
42
|
+
|
|
43
|
+
def to_bytes(self) -> bytes:
|
|
44
|
+
prefix = self.verified_header.to_bytes() if self.verified_header is not None else b""
|
|
45
|
+
return prefix + self.data
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from .patch_header import PatchHeader
|
|
7
|
+
from .body import Body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Patch:
|
|
12
|
+
header: PatchHeader
|
|
13
|
+
body: Body
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def from_bytes(buf: bytes) -> "Patch":
|
|
18
|
+
header = PatchHeader.from_bytes(buf)
|
|
19
|
+
# Everything after the header is the signed body: the verified header
|
|
20
|
+
# (options + rev, Zen only) plus the microcode itself.
|
|
21
|
+
body = Body.from_bytes(buf[header.size:], family=header.cpuid.family)
|
|
22
|
+
return Patch(header=header, body=body)
|
|
23
|
+
|
|
24
|
+
def to_bytes(self) -> bytes:
|
|
25
|
+
"""The whole patch encoding: the header (with signature block, if any)
|
|
26
|
+
followed by the signed body."""
|
|
27
|
+
return self.header.to_bytes() + self.body.to_bytes()
|
|
28
|
+
|
|
29
|
+
def signature_verifies(self, cmac_key: bytes) -> bool | None:
|
|
30
|
+
"""
|
|
31
|
+
Whether the patch signature verifies against its embedded public key,
|
|
32
|
+
using the provided CMAC key (see :data:`Signature.verify`).
|
|
33
|
+
|
|
34
|
+
``None`` for unsigned (pre-Zen) patches. A ``False`` result does not by
|
|
35
|
+
itself mean the patch is forged: patches signed with a CMAC key this
|
|
36
|
+
library does not ship -- Zen 5, and AMD's post-EntrySign patches that
|
|
37
|
+
rotated the key -- also report ``False`` even though their RSA signature
|
|
38
|
+
is structurally valid against the embedded modulus.
|
|
39
|
+
"""
|
|
40
|
+
if self.header.signature is None:
|
|
41
|
+
return None
|
|
42
|
+
return self.header.signature.verify(self.body.to_bytes(), cmac_key)
|
|
43
|
+
|
|
44
|
+
def sha256(self) -> bytes:
|
|
45
|
+
"""SHA-256 digest of the whole patch (header + body)."""
|
|
46
|
+
return hashlib.sha256(self.to_bytes()).digest()
|
|
47
|
+
|
|
48
|
+
def sha256_hex(self) -> str:
|
|
49
|
+
"""SHA-256 digest of the whole patch (header + body) as a hex string."""
|
|
50
|
+
return hashlib.sha256(self.to_bytes()).hexdigest()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from amd_ucode_patch.utils.bcd import decode_bcd, encode_bcd, is_bcd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PatchDate:
|
|
12
|
+
"""
|
|
13
|
+
Build date stored in the first 32-bit word of an AMD microcode patch header.
|
|
14
|
+
|
|
15
|
+
The field is a little-endian, packed binary-coded-decimal value laid out as
|
|
16
|
+
0xMMDDYYYY: the high byte holds the month, the next byte the day, and the low
|
|
17
|
+
16 bits the four-digit year (e.g. 0x12282021 decodes to 2021-12-28).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
FMT = "<HBB" # year (u16), day (u8), month (u8)
|
|
21
|
+
SIZE = struct.calcsize(FMT)
|
|
22
|
+
|
|
23
|
+
YEAR_NIBBLES = 4
|
|
24
|
+
DAY_NIBBLES = 2
|
|
25
|
+
MONTH_NIBBLES = 2
|
|
26
|
+
NIBBLE_COUNT = YEAR_NIBBLES + DAY_NIBBLES + MONTH_NIBBLES
|
|
27
|
+
|
|
28
|
+
# Loose calendar bounds, used only to reject obviously bogus values when
|
|
29
|
+
# probing a file.
|
|
30
|
+
MIN_YEAR = 1990
|
|
31
|
+
MAX_YEAR = 2099
|
|
32
|
+
|
|
33
|
+
# Known AMD patches that ship an out-of-range date but are otherwise
|
|
34
|
+
# legitimate. Treated as plausible despite failing the calendar checks.
|
|
35
|
+
# 2011-13-09 -> family12_cpuid00300F10_rev03000027 (month 13).
|
|
36
|
+
KNOWN_EXCEPTIONS = frozenset({(2011, 13, 9)})
|
|
37
|
+
|
|
38
|
+
year: int
|
|
39
|
+
month: int
|
|
40
|
+
day: int
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_bytes(buf: bytes) -> "PatchDate":
|
|
44
|
+
if len(buf) < PatchDate.SIZE:
|
|
45
|
+
raise ValueError("not enough bytes for AMD patch date")
|
|
46
|
+
raw_year, raw_day, raw_month = struct.unpack(PatchDate.FMT, buf[0:PatchDate.SIZE])
|
|
47
|
+
return PatchDate(
|
|
48
|
+
year=decode_bcd(raw_year, PatchDate.YEAR_NIBBLES),
|
|
49
|
+
day=decode_bcd(raw_day, PatchDate.DAY_NIBBLES),
|
|
50
|
+
month=decode_bcd(raw_month, PatchDate.MONTH_NIBBLES),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def to_bytes(self) -> bytes:
|
|
54
|
+
return struct.pack(
|
|
55
|
+
PatchDate.FMT,
|
|
56
|
+
encode_bcd(self.year, PatchDate.YEAR_NIBBLES),
|
|
57
|
+
encode_bcd(self.day, PatchDate.DAY_NIBBLES),
|
|
58
|
+
encode_bcd(self.month, PatchDate.MONTH_NIBBLES),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def is_valid_encoding(buf: bytes) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Return ``True`` if ``buf`` holds well-formed packed BCD that decodes to a
|
|
65
|
+
plausible calendar date. Intended as a lightweight signature check while
|
|
66
|
+
deciding whether a file looks like a microcode patch.
|
|
67
|
+
"""
|
|
68
|
+
if len(buf) < PatchDate.SIZE:
|
|
69
|
+
return False
|
|
70
|
+
raw_year, raw_day, raw_month = struct.unpack(PatchDate.FMT, buf[0:PatchDate.SIZE])
|
|
71
|
+
if not (
|
|
72
|
+
is_bcd(raw_year, PatchDate.YEAR_NIBBLES)
|
|
73
|
+
and is_bcd(raw_day, PatchDate.DAY_NIBBLES)
|
|
74
|
+
and is_bcd(raw_month, PatchDate.MONTH_NIBBLES)
|
|
75
|
+
):
|
|
76
|
+
return False
|
|
77
|
+
return PatchDate.from_bytes(buf).is_plausible()
|
|
78
|
+
|
|
79
|
+
def is_plausible(self) -> bool:
|
|
80
|
+
"""Return ``True`` if the components fall within sane calendar ranges."""
|
|
81
|
+
if (self.year, self.month, self.day) in PatchDate.KNOWN_EXCEPTIONS:
|
|
82
|
+
return True
|
|
83
|
+
return (
|
|
84
|
+
1 <= self.month <= 12
|
|
85
|
+
and 1 <= self.day <= 31
|
|
86
|
+
and PatchDate.MIN_YEAR <= self.year <= PatchDate.MAX_YEAR
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
|