Merge pull request #27621 from esposem/ukify_read
ukify: add an option to read the PE sections
This commit is contained in:
+42
-6
@@ -118,6 +118,20 @@
|
||||
|
||||
<para>The output files must not exist.</para>
|
||||
</refsect2>
|
||||
|
||||
<refsect2>
|
||||
<title><command>inspect</command></title>
|
||||
|
||||
<para>Display information about the sections in a given binary or binaries.
|
||||
If <option>--all</option> is given, all sections are shown.
|
||||
Otherwise, if <option>--section=</option> option is specified at least once, only those sections are shown.
|
||||
Otherwise, well-known sections that are typically included in an UKI are shown.
|
||||
For each section, its name, size, and sha256-digest is printed.
|
||||
For text sections, the contents are printed.</para>
|
||||
|
||||
<para>Also see the description of <option>-j</option>/<option>--json=</option> and
|
||||
<option>--section=</option>.</para>
|
||||
</refsect2>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
@@ -163,13 +177,23 @@
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
|
||||
<term><option>--section=<replaceable>NAME</replaceable>:<arg choice="plain">text|binary</arg><optional>@<replaceable>PATH</replaceable></optional></option></term>
|
||||
|
||||
<listitem><para>Specify an arbitrary additional section
|
||||
<literal><replaceable>NAME</replaceable></literal>. Note that the name is used as-is, and if the
|
||||
section name should start with a dot, it must be included in <replaceable>NAME</replaceable>. The
|
||||
argument may be a literal string, or <literal>@</literal> followed by a path name. This option may be
|
||||
specified more than once. Any sections specified in this fashion will be inserted (in order) before
|
||||
the <literal>.linux</literal> section which is always last.</para></listitem>
|
||||
<listitem><para>For all verbs except <command>inspect</command>, the first syntax is used.
|
||||
Specify an arbitrary additional section <literal><replaceable>NAME</replaceable></literal>.
|
||||
The argument may be a literal string, or <literal>@</literal> followed by a path name.
|
||||
This option may be specified more than once. Any sections specified in this fashion will be
|
||||
inserted (in order) before the <literal>.linux</literal> section which is always last.</para>
|
||||
|
||||
<para>For the <command>inspect</command> verb, the second syntax is used.
|
||||
The section <replaceable>NAME</replaceable> will be inspected (if found).
|
||||
If the second argument is <literal>text</literal>, the contents will be printed.
|
||||
If the third argument is given, the contents will be saved to file <replaceable>PATH</replaceable>.
|
||||
</para>
|
||||
|
||||
<para>Note that the name is used as-is, and if the section name should start with a dot, it must be
|
||||
included in <replaceable>NAME</replaceable>.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
@@ -196,6 +220,18 @@
|
||||
form the configuration file and the commandline are combined.</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--all</option></term>
|
||||
|
||||
<listitem><para>Print all sections (with <command>inspect</command> verb).</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--json</option></term>
|
||||
|
||||
<listitem><para>Generate JSON output (with <command>inspect</command> verb).</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<xi:include href="standard-options.xml" xpointer="help" />
|
||||
<xi:include href="standard-options.xml" xpointer="version" />
|
||||
</variablelist>
|
||||
|
||||
@@ -360,6 +360,13 @@ def test_help(capsys):
|
||||
assert '--section' in out.out
|
||||
assert not out.err
|
||||
|
||||
def test_help_display(capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
ukify.parse_args(['inspect', '--help'])
|
||||
out = capsys.readouterr()
|
||||
assert '--section' in out.out
|
||||
assert not out.err
|
||||
|
||||
def test_help_error_deprecated(capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
ukify.parse_args(['a', 'b', '--no-such-option'])
|
||||
@@ -603,6 +610,52 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
|
||||
|
||||
assert f"The signer's common name is {author}" in dump
|
||||
|
||||
def test_inspect(kernel_initrd, tmpdir, capsys):
|
||||
if kernel_initrd is None:
|
||||
pytest.skip('linux+initrd not found')
|
||||
if not shutil.which('sbsign'):
|
||||
pytest.skip('sbsign not found')
|
||||
|
||||
ourdir = pathlib.Path(__file__).parent
|
||||
cert = unbase64(ourdir / 'example.signing.crt.base64')
|
||||
key = unbase64(ourdir / 'example.signing.key.base64')
|
||||
|
||||
output = f'{tmpdir}/signed2.efi'
|
||||
uname_arg='1.2.3'
|
||||
osrel_arg='Linux'
|
||||
cmdline_arg='ARG1 ARG2 ARG3'
|
||||
opts = ukify.parse_args([
|
||||
'build',
|
||||
*kernel_initrd,
|
||||
f'--cmdline={cmdline_arg}',
|
||||
f'--os-release={osrel_arg}',
|
||||
f'--uname={uname_arg}',
|
||||
f'--output={output}',
|
||||
f'--secureboot-certificate={cert.name}',
|
||||
f'--secureboot-private-key={key.name}',
|
||||
])
|
||||
|
||||
ukify.check_inputs(opts)
|
||||
ukify.make_uki(opts)
|
||||
|
||||
opts = ukify.parse_args(['inspect', output])
|
||||
ukify.inspect_sections(opts)
|
||||
|
||||
text = capsys.readouterr().out
|
||||
|
||||
expected_osrel = f'.osrel:\n size: {len(osrel_arg)}'
|
||||
assert expected_osrel in text
|
||||
expected_cmdline = f'.cmdline:\n size: {len(cmdline_arg)}'
|
||||
assert expected_cmdline in text
|
||||
expected_uname = f'.uname:\n size: {len(uname_arg)}'
|
||||
assert expected_uname in text
|
||||
|
||||
expected_initrd = '.initrd:\n size:'
|
||||
assert expected_initrd in text
|
||||
expected_linux = '.linux:\n size:'
|
||||
assert expected_linux in text
|
||||
|
||||
|
||||
def test_pcr_signing(kernel_initrd, tmpdir):
|
||||
if kernel_initrd is None:
|
||||
pytest.skip('linux+initrd not found')
|
||||
|
||||
+153
-37
@@ -43,6 +43,8 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from hashlib import sha256
|
||||
from typing import (Any,
|
||||
Callable,
|
||||
IO,
|
||||
@@ -89,7 +91,7 @@ def guess_efi_arch():
|
||||
if int(size) == 32:
|
||||
efi_arch = fallback[0]
|
||||
|
||||
print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
|
||||
# print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
|
||||
return efi_arch
|
||||
|
||||
|
||||
@@ -243,13 +245,26 @@ class Uname:
|
||||
print(str(e))
|
||||
return None
|
||||
|
||||
DEFAULT_SECTIONS_TO_SHOW = {
|
||||
'.linux' : 'binary',
|
||||
'.initrd' : 'binary',
|
||||
'.splash' : 'binary',
|
||||
'.dt' : 'binary',
|
||||
'.cmdline' : 'text',
|
||||
'.osrel' : 'text',
|
||||
'.uname' : 'text',
|
||||
'.pcrpkey' : 'text',
|
||||
'.pcrsig' : 'text',
|
||||
'.sbat' : 'text',
|
||||
}
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Section:
|
||||
name: str
|
||||
content: pathlib.Path
|
||||
content: Optional[pathlib.Path]
|
||||
tmpfile: Optional[IO] = None
|
||||
measure: bool = False
|
||||
output_mode: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, contents, **kwargs):
|
||||
@@ -265,7 +280,7 @@ class Section:
|
||||
return cls(name, contents, tmpfile=tmp, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, s):
|
||||
def parse_input(cls, s):
|
||||
try:
|
||||
name, contents, *rest = s.split(':')
|
||||
except ValueError as e:
|
||||
@@ -276,7 +291,19 @@ class Section:
|
||||
if contents.startswith('@'):
|
||||
contents = pathlib.Path(contents[1:])
|
||||
|
||||
return cls.create(name, contents)
|
||||
sec = cls.create(name, contents)
|
||||
sec.check_name()
|
||||
return sec
|
||||
|
||||
@classmethod
|
||||
def parse_output(cls, s):
|
||||
if not (m := re.match(r'([a-zA-Z0-9_.]+):(text|binary)(?:@(.+))?', s)):
|
||||
raise ValueError(f'Cannot parse section spec: {s!r}')
|
||||
|
||||
name, ttype, out = m.groups()
|
||||
out = pathlib.Path(out) if out else None
|
||||
|
||||
return cls.create(name, out, output_mode=ttype)
|
||||
|
||||
def size(self):
|
||||
return self.content.stat().st_size
|
||||
@@ -551,6 +578,7 @@ def pe_add_sections(uki: UKI, output: str):
|
||||
if offset + new_section.sizeof() > pe.OPTIONAL_HEADER.SizeOfHeaders:
|
||||
raise PEError(f'Not enough header space to add section {section.name}.')
|
||||
|
||||
assert section.content
|
||||
data = section.content.read_bytes()
|
||||
|
||||
new_section.set_file_offset(offset)
|
||||
@@ -696,31 +724,32 @@ def make_uki(opts):
|
||||
# kernel payload signing
|
||||
|
||||
sign_tool = None
|
||||
if opts.signtool == 'sbsign':
|
||||
sign_tool = find_sbsign(opts=opts)
|
||||
sign = sbsign_sign
|
||||
verify_tool = SBVERIFY
|
||||
else:
|
||||
sign_tool = find_pesign(opts=opts)
|
||||
sign = pesign_sign
|
||||
verify_tool = PESIGCHECK
|
||||
|
||||
sign_args_present = opts.sb_key or opts.sb_cert_name
|
||||
|
||||
if sign_tool is None and sign_args_present:
|
||||
raise ValueError(f'{opts.signtool}, required for signing, is not installed')
|
||||
|
||||
sign_kernel = opts.sign_kernel
|
||||
if sign_kernel is None and opts.linux is not None and sign_args_present:
|
||||
# figure out if we should sign the kernel
|
||||
sign_kernel = verify(verify_tool, opts)
|
||||
sign = None
|
||||
linux = opts.linux
|
||||
|
||||
if sign_kernel:
|
||||
linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed')
|
||||
linux = pathlib.Path(linux_signed.name)
|
||||
sign(sign_tool, opts.linux, linux, opts=opts)
|
||||
else:
|
||||
linux = opts.linux
|
||||
if sign_args_present:
|
||||
if opts.signtool == 'sbsign':
|
||||
sign_tool = find_sbsign(opts=opts)
|
||||
sign = sbsign_sign
|
||||
verify_tool = SBVERIFY
|
||||
else:
|
||||
sign_tool = find_pesign(opts=opts)
|
||||
sign = pesign_sign
|
||||
verify_tool = PESIGCHECK
|
||||
|
||||
if sign_tool is None:
|
||||
raise ValueError(f'{opts.signtool}, required for signing, is not installed')
|
||||
|
||||
if sign_kernel is None and opts.linux is not None:
|
||||
# figure out if we should sign the kernel
|
||||
sign_kernel = verify(verify_tool, opts)
|
||||
|
||||
if sign_kernel:
|
||||
linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed')
|
||||
linux = pathlib.Path(linux_signed.name)
|
||||
sign(sign_tool, opts.linux, linux, opts=opts)
|
||||
|
||||
if opts.uname is None and opts.linux is not None:
|
||||
print('Kernel version not specified, starting autodetection 😖.')
|
||||
@@ -782,16 +811,17 @@ uki,1,UKI,uki,1,https://www.freedesktop.org/software/systemd/man/systemd-stub.ht
|
||||
|
||||
if sign_args_present:
|
||||
unsigned = tempfile.NamedTemporaryFile(prefix='uki')
|
||||
output = unsigned.name
|
||||
unsigned_output = unsigned.name
|
||||
else:
|
||||
output = opts.output
|
||||
unsigned_output = opts.output
|
||||
|
||||
pe_add_sections(uki, output)
|
||||
pe_add_sections(uki, unsigned_output)
|
||||
|
||||
# UKI signing
|
||||
|
||||
if sign_args_present:
|
||||
sign(sign_tool, unsigned.name, opts.output, opts=opts)
|
||||
assert sign
|
||||
sign(sign_tool, unsigned_output, opts.output, opts=opts)
|
||||
|
||||
# We end up with no executable bits, let's reapply them
|
||||
os.umask(umask := os.umask(0))
|
||||
@@ -913,6 +943,59 @@ def generate_keys(opts):
|
||||
pub_key.write_bytes(pub_key_pem)
|
||||
|
||||
|
||||
def inspect_section(opts, section):
|
||||
name = section.Name.rstrip(b"\x00").decode()
|
||||
|
||||
# find the config for this section in opts and whether to show it
|
||||
config = opts.sections_by_name.get(name, None)
|
||||
show = (config or
|
||||
opts.all or
|
||||
(name in DEFAULT_SECTIONS_TO_SHOW and not opts.sections))
|
||||
if not show:
|
||||
return name, None
|
||||
|
||||
ttype = config.output_mode if config else DEFAULT_SECTIONS_TO_SHOW.get(name, 'binary')
|
||||
|
||||
data = section.get_data(ignore_padding=True)
|
||||
size = section.Misc_VirtualSize
|
||||
digest = sha256(data).hexdigest()
|
||||
|
||||
struct = {
|
||||
'size' : size,
|
||||
'sha256' : digest,
|
||||
}
|
||||
|
||||
if ttype == 'text':
|
||||
try:
|
||||
struct['text'] = data.decode()
|
||||
except UnicodeDecodeError as e:
|
||||
print(f"Section {name!r} is not valid text: {e}")
|
||||
struct['text'] = '(not valid UTF-8)'
|
||||
|
||||
if config and config.content:
|
||||
assert isinstance(config.content, pathlib.Path)
|
||||
config.content.write_bytes(data)
|
||||
|
||||
if opts.json == 'off':
|
||||
print(f"{name}:\n size: {size} bytes\n sha256: {digest}")
|
||||
if ttype == 'text':
|
||||
text = textwrap.indent(struct['text'].rstrip(), ' ' * 4)
|
||||
print(f" text:\n{text}")
|
||||
|
||||
return name, struct
|
||||
|
||||
|
||||
def inspect_sections(opts):
|
||||
indent = 4 if opts.json == 'pretty' else None
|
||||
|
||||
for file in opts.files:
|
||||
pe = pefile.PE(file, fast_load=True)
|
||||
gen = (inspect_section(opts, section) for section in pe.sections)
|
||||
descs = {key:val for (key, val) in gen if val}
|
||||
if opts.json != 'off':
|
||||
json.dump(descs, sys.stdout, indent=indent)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ConfigItem:
|
||||
@staticmethod
|
||||
@@ -983,6 +1066,7 @@ class ConfigItem:
|
||||
default: Any = None
|
||||
version: Optional[str] = None
|
||||
choices: Optional[tuple[str, ...]] = None
|
||||
const: Optional[Any] = None
|
||||
help: Optional[str] = None
|
||||
|
||||
# metadata for config file parsing
|
||||
@@ -1045,7 +1129,7 @@ class ConfigItem:
|
||||
return (section_name, key, value)
|
||||
|
||||
|
||||
VERBS = ('build', 'genkey')
|
||||
VERBS = ('build', 'genkey', 'inspect')
|
||||
|
||||
CONFIG_ITEMS = [
|
||||
ConfigItem(
|
||||
@@ -1160,10 +1244,9 @@ CONFIG_ITEMS = [
|
||||
'--section',
|
||||
dest = 'sections',
|
||||
metavar = 'NAME:TEXT|@PATH',
|
||||
type = Section.parse_arg,
|
||||
action = 'append',
|
||||
default = [],
|
||||
help = 'additional section as name and contents [NAME section]',
|
||||
help = 'section as name and contents [NAME section] or section to print',
|
||||
),
|
||||
|
||||
ConfigItem(
|
||||
@@ -1277,6 +1360,26 @@ CONFIG_ITEMS = [
|
||||
action = argparse.BooleanOptionalAction,
|
||||
help = 'print systemd-measure output for the UKI',
|
||||
),
|
||||
|
||||
ConfigItem(
|
||||
'--json',
|
||||
choices = ('pretty', 'short', 'off'),
|
||||
default = 'off',
|
||||
help = 'generate JSON output',
|
||||
),
|
||||
ConfigItem(
|
||||
'-j',
|
||||
dest='json',
|
||||
action='store_const',
|
||||
const='pretty',
|
||||
help='equivalent to --json=pretty',
|
||||
),
|
||||
|
||||
ConfigItem(
|
||||
'--all',
|
||||
help = 'print all sections',
|
||||
action = 'store_true',
|
||||
),
|
||||
]
|
||||
|
||||
CONFIGFILE_ITEMS = { item.config_key:item
|
||||
@@ -1381,7 +1484,15 @@ def finalize_options(opts):
|
||||
# Figure out which syntax is being used, one of:
|
||||
# ukify verb --arg --arg --arg
|
||||
# ukify linux initrd…
|
||||
if len(opts.positional) == 1 and opts.positional[0] in VERBS:
|
||||
if opts.positional[0] == 'inspect':
|
||||
opts.verb = opts.positional[0]
|
||||
opts.files = opts.positional[1:]
|
||||
if not opts.files:
|
||||
raise ValueError('file(s) to inspect must be specified')
|
||||
if len(opts.files) > 1 and opts.json != 'off':
|
||||
# We could allow this in the future, but we need to figure out the right structure
|
||||
raise ValueError('JSON output is not allowed with multiple files')
|
||||
elif len(opts.positional) == 1 and opts.positional[0] in VERBS:
|
||||
opts.verb = opts.positional[0]
|
||||
elif opts.linux or opts.initrd:
|
||||
raise ValueError('--linux/--initrd options cannot be used with positional arguments')
|
||||
@@ -1439,7 +1550,7 @@ def finalize_options(opts):
|
||||
raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together when using --signtool=sbsign')
|
||||
else:
|
||||
if not bool(opts.sb_cert_name):
|
||||
raise ValueError('--certificate-name must be specified when using --signtool=pesign')
|
||||
raise ValueError('--secureboot-certificate-name must be specified when using --signtool=pesign')
|
||||
|
||||
if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name:
|
||||
raise ValueError('--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified')
|
||||
@@ -1450,8 +1561,11 @@ def finalize_options(opts):
|
||||
suffix = '.efi' if opts.sb_key or opts.sb_cert_name else '.unsigned.efi'
|
||||
opts.output = opts.linux.name + suffix
|
||||
|
||||
for section in opts.sections:
|
||||
section.check_name()
|
||||
# Now that we know if we're inputting or outputting, really parse section config
|
||||
f = Section.parse_output if opts.verb == 'inspect' else Section.parse_input
|
||||
opts.sections = [f(s) for s in opts.sections]
|
||||
# A convenience dictionary to make it easy to look up sections
|
||||
opts.sections_by_name = {s.name:s for s in opts.sections}
|
||||
|
||||
if opts.summary:
|
||||
# TODO: replace pprint() with some fancy formatting.
|
||||
@@ -1474,6 +1588,8 @@ def main():
|
||||
elif opts.verb == 'genkey':
|
||||
check_cert_and_keys_nonexistent(opts)
|
||||
generate_keys(opts)
|
||||
elif opts.verb == 'inspect':
|
||||
inspect_sections(opts)
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user