Skip to content

Commit ba039eb

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 ba039eb

5 files changed

Lines changed: 527 additions & 53 deletions

File tree

mkosi/__init__.py

Lines changed: 97 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,100 @@ 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+
for file in seen_bind_paths:
1832+
options += ["--ro-bind", file, workdir(file)]
1833+
if need_run_bind:
1834+
options += ["--bind", "/run", "/run"]
18131835
elif ArtifactOutput.pcrs in context.config.split_artifacts:
18141836
assert context.config.sign_expected_pcr_certificate
18151837

18161838
json_out = True
1839+
cert = context.config.sign_expected_pcr_certificate
18171840
arguments += [
18181841
"--policy-digest",
18191842
"--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
1843+
"--pcr-certificate", workdir(cert),
18241844
] # fmt: skip
1845+
options += ["--ro-bind", cert, workdir(cert)]
1846+
seen_bind_paths.add(cert)
1847+
1848+
if (pcrpkey := context.config.sign_expected_pcr_uki_public_key) is not None:
1849+
if not pcrpkey.exists():
1850+
die(f"SignExpectedPcrUKIPublicKey={pcrpkey} does not exist")
1851+
arguments += ["--pcrpkey", workdir(pcrpkey)]
1852+
if pcrpkey not in seen_bind_paths:
1853+
options += ["--ro-bind", pcrpkey, workdir(pcrpkey)]
1854+
seen_bind_paths.add(pcrpkey)
18251855

18261856
if microcodes:
18271857
# new .ucode section support?
@@ -2723,25 +2753,39 @@ def check_inputs(config: Config) -> None:
27232753
hint="Run mkosi genkey to generate a key/certificate pair",
27242754
)
27252755

2726-
if config.sign_expected_pcr == ConfigFeature.enabled and not config.sign_expected_pcr_key:
2756+
if (
2757+
config.sign_expected_pcr == ConfigFeature.enabled
2758+
and not config.sign_expected_pcr_with_phases
2759+
and not config.sign_expected_pcr_key
2760+
):
27272761
die(
27282762
"SignExpectedPcr= is enabled but no private key is configured",
27292763
hint="Run mkosi genkey to generate a key/certificate pair",
27302764
)
27312765

2732-
if config.sign_expected_pcr == ConfigFeature.enabled and not config.sign_expected_pcr_certificate:
2766+
if (
2767+
config.sign_expected_pcr == ConfigFeature.enabled
2768+
and not config.sign_expected_pcr_with_phases
2769+
and not config.sign_expected_pcr_certificate
2770+
):
27332771
die(
27342772
"SignExpectedPcr= is enabled but no certificate is configured",
27352773
hint="Run mkosi genkey to generate a key/certificate pair",
27362774
)
27372775

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

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
2785+
if config.secure_boot_certificate_source != config.sign_expected_pcr_certificate_source:
2786+
die(
2787+
"Secure boot certificate source and expected PCR signatures certificate source have to be the same" # noqa: E501
2788+
) # fmt: skip
27452789

27462790
if config.verity == Verity.signed and not config.verity_key:
27472791
die(

0 commit comments

Comments
 (0)