Skip to content

Commit 1981f1e

Browse files
committed
ukify: Support multiple PCR signing keys to sign phases separately
To be able to bind a secret against a signed PCR policy so that it only unlocks in the initrd but not the final system one has to sign the policies with separate keys because that's what one actually binds to. This is supported in ukify as "phases" and ukify accepts the PCR signing keys specified multiple times together with the phase they are for. We can't extend the existing config setting for this scheme, so instead we have to add a new setting which can hold the configuration entries. Also, we need a new setting to specify which PCR pub key goes into the .pcrpkey UKI section because when we have many, ukify won't add any by default. Add SignExpectedPcrWithPhases= as alternative to SignExpectedPcrKey=/ SignExpectedPcrCertificate=/*Source= where we can now specify the phase signing configuration one line each. A line contains KEY CERT PHASES [KEYSRC [CERTSRC]] where KEYSRC/CERTSRC defaults to "file". One can also set "-" for PHASES to omit --phases for ukify so that it chooses the defaults. Also add SignExpectedPcrUKIPublicKey= to be able to specify the key for the .pcrpkey UKI section. Closes #4109
1 parent 23913f2 commit 1981f1e

5 files changed

Lines changed: 525 additions & 53 deletions

File tree

mkosi/__init__.py

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
ManifestFormat,
6464
Network,
6565
OutputFormat,
66+
PcrSigner,
6667
SecureBootSignTool,
6768
ShimBootloader,
6869
Ssh,
@@ -1583,8 +1584,10 @@ def want_signed_pcrs(config: Config) -> bool:
15831584
return config.sign_expected_pcr == ConfigFeature.enabled or (
15841585
config.sign_expected_pcr == ConfigFeature.auto
15851586
and config.find_binary("systemd-measure", "/usr/lib/systemd/systemd-measure") is not None
1586-
and bool(config.sign_expected_pcr_key)
1587-
and bool(config.sign_expected_pcr_certificate)
1587+
and (
1588+
(bool(config.sign_expected_pcr_key) and bool(config.sign_expected_pcr_certificate))
1589+
or bool(config.sign_expected_pcr_with_phases)
1590+
)
15881591
)
15891592

15901593

@@ -1755,73 +1758,98 @@ def build_uki(
17551758

17561759
arguments += ["--sign-kernel"]
17571760

1758-
if want_signed_pcrs(context.config):
1759-
assert context.config.sign_expected_pcr_key
1760-
assert context.config.sign_expected_pcr_certificate
1761+
seen_bind_paths: set[Path] = set()
17611762

1763+
if want_signed_pcrs(context.config):
17621764
arguments += [
17631765
# SHA1 might be disabled in OpenSSL depending on the distro so we opt to not sign
17641766
# for SHA1 to avoid having to manage a bunch of configuration to re-enable SHA1.
17651767
"--pcr-banks", "sha256",
17661768
] # fmt: skip
17671769

1768-
if (
1769-
systemd_tool_version(
1770-
python_binary(context.config),
1771-
ukify,
1772-
sandbox=context.sandbox,
1773-
)
1774-
>= "258"
1775-
):
1770+
ukify_version = systemd_tool_version(
1771+
python_binary(context.config),
1772+
ukify,
1773+
sandbox=context.sandbox,
1774+
)
1775+
1776+
if ukify_version >= "258":
17761777
cert_parameter = "--pcr-certificate"
17771778
else:
17781779
cert_parameter = "--pcr-public-key"
17791780

1780-
# If we're providing the private key via an engine or provider, we have to pass in a X.509
1781-
# certificate via --pcr-certificate as well.
1782-
if context.config.sign_expected_pcr_key_source.type != KeySourceType.file:
1783-
if context.config.sign_expected_pcr_certificate_source.type == CertificateSourceType.provider:
1784-
arguments += [
1785-
"--certificate-provider",
1786-
f"provider:{context.config.sign_expected_pcr_certificate_source.source}",
1787-
]
1781+
if context.config.sign_expected_pcr_with_phases:
1782+
signers = context.config.sign_expected_pcr_with_phases
1783+
if ukify_version < "253":
1784+
die(f"ukify {ukify_version} does not support --phases (need >= 253)")
1785+
else:
1786+
assert context.config.sign_expected_pcr_key
1787+
assert context.config.sign_expected_pcr_certificate
1788+
signers = [
1789+
PcrSigner(
1790+
key=context.config.sign_expected_pcr_key,
1791+
certificate=context.config.sign_expected_pcr_certificate,
1792+
phases=[],
1793+
key_source=context.config.sign_expected_pcr_key_source,
1794+
certificate_source=context.config.sign_expected_pcr_certificate_source,
1795+
)
1796+
]
17881797

1789-
options += ["--bind", "/run", "/run"]
1798+
need_run_bind = False
17901799

1791-
if context.config.sign_expected_pcr_certificate.exists():
1792-
arguments += [
1793-
cert_parameter, workdir(context.config.sign_expected_pcr_certificate),
1794-
] # fmt: skip
1795-
options += [
1796-
"--ro-bind", context.config.sign_expected_pcr_certificate, workdir(context.config.sign_expected_pcr_certificate), # noqa: E501
1797-
] # fmt: skip
1800+
for signer in signers:
1801+
# If we're providing the private key via an engine or provider, we have to pass in
1802+
# a X.509 certificate via --pcr-certificate as well.
1803+
if signer.key_source.type != KeySourceType.file:
1804+
if signer.certificate_source.type == CertificateSourceType.provider:
1805+
arguments += [
1806+
"--certificate-provider", f"provider:{signer.certificate_source.source}",
1807+
] # fmt: skip
1808+
1809+
need_run_bind = True
1810+
1811+
if signer.certificate.exists():
1812+
arguments += [cert_parameter, workdir(signer.certificate)]
1813+
seen_bind_paths.add(signer.certificate)
1814+
else:
1815+
arguments += [cert_parameter, signer.certificate]
1816+
1817+
if signer.key_source.type == KeySourceType.engine:
1818+
arguments += ["--signing-engine", signer.key_source.source]
1819+
elif signer.key_source.type == KeySourceType.provider:
1820+
arguments += ["--signing-provider", signer.key_source.source]
1821+
1822+
if signer.key.exists():
1823+
arguments += ["--pcr-private-key", workdir(signer.key)]
1824+
seen_bind_paths.add(signer.key)
17981825
else:
1799-
arguments += [cert_parameter, context.config.sign_expected_pcr_certificate]
1826+
arguments += ["--pcr-private-key", signer.key]
18001827

1801-
if context.config.sign_expected_pcr_key_source.type == KeySourceType.engine:
1802-
arguments += ["--signing-engine", context.config.sign_expected_pcr_key_source.source]
1803-
elif context.config.sign_expected_pcr_key_source.type == KeySourceType.provider:
1804-
arguments += ["--signing-provider", context.config.sign_expected_pcr_key_source.source]
1828+
if signer.phases:
1829+
arguments += ["--phases", " ".join(signer.phases)]
18051830

1806-
if context.config.sign_expected_pcr_key.exists():
1807-
arguments += ["--pcr-private-key", workdir(context.config.sign_expected_pcr_key)]
1808-
options += [
1809-
"--ro-bind", context.config.sign_expected_pcr_key, workdir(context.config.sign_expected_pcr_key), # noqa: E501
1810-
] # fmt: skip
1811-
else:
1812-
arguments += ["--pcr-private-key", context.config.sign_expected_pcr_key]
1831+
if need_run_bind:
1832+
options += ["--bind", "/run", "/run"]
18131833
elif ArtifactOutput.pcrs in context.config.split_artifacts:
18141834
assert context.config.sign_expected_pcr_certificate
18151835

18161836
json_out = True
1837+
cert = context.config.sign_expected_pcr_certificate
18171838
arguments += [
18181839
"--policy-digest",
18191840
"--pcr-banks", "sha256",
1820-
"--pcr-certificate", workdir(context.config.sign_expected_pcr_certificate),
1821-
] # fmt: skip
1822-
options += [
1823-
"--ro-bind", context.config.sign_expected_pcr_certificate, workdir(context.config.sign_expected_pcr_certificate), # noqa: E501
1841+
"--pcr-certificate", workdir(cert),
18241842
] # fmt: skip
1843+
seen_bind_paths.add(cert)
1844+
1845+
if (pcrpkey := context.config.sign_expected_pcr_uki_public_key) is not None:
1846+
if not pcrpkey.exists():
1847+
die(f"SignExpectedPcrUKIPublicKey={pcrpkey} does not exist")
1848+
arguments += ["--pcrpkey", workdir(pcrpkey)]
1849+
seen_bind_paths.add(pcrpkey)
1850+
1851+
for file in seen_bind_paths:
1852+
options += ["--ro-bind", file, workdir(file)]
18251853

18261854
if microcodes:
18271855
# new .ucode section support?
@@ -2723,25 +2751,39 @@ def check_inputs(config: Config) -> None:
27232751
hint="Run mkosi genkey to generate a key/certificate pair",
27242752
)
27252753

2726-
if config.sign_expected_pcr == ConfigFeature.enabled and not config.sign_expected_pcr_key:
2754+
if (
2755+
config.sign_expected_pcr == ConfigFeature.enabled
2756+
and not config.sign_expected_pcr_with_phases
2757+
and not config.sign_expected_pcr_key
2758+
):
27272759
die(
27282760
"SignExpectedPcr= is enabled but no private key is configured",
27292761
hint="Run mkosi genkey to generate a key/certificate pair",
27302762
)
27312763

2732-
if config.sign_expected_pcr == ConfigFeature.enabled and not config.sign_expected_pcr_certificate:
2764+
if (
2765+
config.sign_expected_pcr == ConfigFeature.enabled
2766+
and not config.sign_expected_pcr_with_phases
2767+
and not config.sign_expected_pcr_certificate
2768+
):
27332769
die(
27342770
"SignExpectedPcr= is enabled but no certificate is configured",
27352771
hint="Run mkosi genkey to generate a key/certificate pair",
27362772
)
27372773

2738-
if config.secure_boot_key_source != config.sign_expected_pcr_key_source:
2739-
die("Secure boot key source and expected PCR signatures key source have to be the same")
2774+
# When SignExpectedPcrWithPhases= is used the key/certificate sources live per-entry, so the
2775+
# scalar sign_expected_pcr_{key,certificate}_source fields default to "file" regardless of
2776+
# what the per-signer sources are. Comparing those scalars against secure boot's sources
2777+
# would be meaningless, so skip the check in that case and rely on ukify to reject
2778+
# inconsistent sources at invocation time.
2779+
if not config.sign_expected_pcr_with_phases:
2780+
if config.secure_boot_key_source != config.sign_expected_pcr_key_source:
2781+
die("Secure boot key source and expected PCR signatures key source have to be the same")
27402782

2741-
if config.secure_boot_certificate_source != config.sign_expected_pcr_certificate_source:
2742-
die(
2743-
"Secure boot certificate source and expected PCR signatures certificate source have to be the same" # noqa: E501
2744-
) # fmt: skip
2783+
if config.secure_boot_certificate_source != config.sign_expected_pcr_certificate_source:
2784+
die(
2785+
"Secure boot certificate source and expected PCR signatures certificate source have to be the same" # noqa: E501
2786+
) # fmt: skip
27452787

27462788
if config.verity == Verity.signed and not config.verity_key:
27472789
die(

0 commit comments

Comments
 (0)