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