Starts an instance of swtpm per VM in it's systemd scope, it will terminate by itself if the VM exits, or be terminated manually if startup fails.
Before first use, a TPM state is created via swtpm_setup. The state lives in "/etc/pve/priv/tpm/<vmid>-<version>/". TPM state is cleared if the 'tpm' config option is removed or the version changed, effectively clearing any stored keys/data. Signed-off-by: Stefan Reiter <s.rei...@proxmox.com> --- PVE/QemuServer.pm | 134 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index b0fe257..76a25ae 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -686,6 +686,12 @@ EODESCR description => "Configure a VirtIO-based Random Number Generator.", optional => 1, }, + tpm => { + optional => 1, + type => 'string', + enum => [ qw(v1.2 v2.0) ], + description => "Configure an emulated Trusted Platform Module.", + }, }; my $cicustom_fmt = { @@ -2945,6 +2951,116 @@ sub audio_devs { return $devs; } +sub get_tpm_paths { + my ($vmid, $version) = @_; + return { + state => "/etc/pve/priv/tpm/$vmid-$version/", + socket => "/var/run/qemu-server/$vmid.swtpm", + pid => "/var/run/qemu-server/$vmid.swtpm.pid", + filename => $version eq "v1.2" ? "tpm-00.permall" : "tpm2-00.permall", + }; +} + +sub print_tpm_device { + my ($vmid, $version) = @_; + my $paths = get_tpm_paths($vmid, $version); + + my $devs = []; + + push @$devs, "-chardev", "socket,id=tpmchar,path=$paths->{socket}"; + push @$devs, "-tpmdev", "emulator,id=tpmdev,chardev=tpmchar"; + push @$devs, "-device", "tpm-tis,tpmdev=tpmdev"; + + return $devs; +} + +sub start_swtpm { + my ($vmid, $version, $migration) = @_; + my $paths = get_tpm_paths($vmid, $version); + + if ($migration) { + # we will get migration state from remote, so remove any pre-existing + clear_tpm_states($vmid); + File::Path::make_path($paths->{state}); + } else { + # run swtpm_setup to create a new TPM state if it doesn't exist yet + if (! -f "$paths->{state}/$paths->{filename}") { + print "Creating new TPM state\n"; + + # swtpm_setup does not like /etc/pve/priv, so create in tempdir + my $tmppath = "/tmp/tpm-$vmid-$$"; + File::Path::make_path($tmppath, mode => 0600); + my $setup_cmd = [ + "swtpm_setup", + "--tpmstate", + "$tmppath", + "--createek", + "--create-ek-cert", + "--create-platform-cert", + "--lock-nvram", + "--config", + "/etc/swtpm_setup.conf", # do not use XDG configs + "--runas", + "0", # force creation as root, error if not possible + ]; + + push @$setup_cmd, "--tpm2" if $version eq 'v2.0'; + # TPM 2.0 supports ECC crypto, use if possible + push @$setup_cmd, "--ecc" if $version eq 'v2.0'; + + # produces a lot of verbose output, only show on error + my $tpmout = ""; + run_command($setup_cmd, outfunc => sub { + $tpmout .= $1 . "\n"; + }); + + File::Path::make_path($paths->{state}); + my $res = File::Copy::move("$tmppath/$paths->{filename}", + "$paths->{state}/$paths->{filename}"); + File::Path::rmtree($tmppath); + if (!$res) { + my $err = $!; + File::Path::rmtree($tmppath); + print "swtpm_setup reported:\n$tpmout"; + die "couldn't move TPM state into '$paths->{state}' - $err\n"; + } + } + } + + my $emulator_cmd = [ + "swtpm", + "socket", + "--tpmstate", + "dir=$paths->{state},mode=0600", + "--ctrl", + "type=unixio,path=$paths->{socket},mode=0600", + "--pid", + "file=$paths->{pid}", + "--terminate", # terminate on QEMU disconnect + "--daemon", + ]; + push @$emulator_cmd, "--tpm2" if $version eq 'v2.0'; + run_command($emulator_cmd); + + # return untainted PID of swtpm daemon so it can be killed on error + file_read_firstline($paths->{pid}) =~ m/(\d+)/; + return $1; +} + +# clear any TPM states other than the ones relevant for $version +sub clear_tpm_states { + my ($vmid, $keep_version) = @_; + + my $clear = sub { + my ($v) = @_; + my $paths = get_tpm_paths($vmid, $v); + rmtree $paths->{state}; + }; + + &$clear("v1.2") if !$keep_version || $keep_version ne "v1.2"; + &$clear("v2.0") if !$keep_version || $keep_version ne "v2.0"; +} + sub vga_conf_has_spice { my ($vga) = @_; @@ -3446,6 +3562,11 @@ sub config_to_command { push @$devices, @$audio_devs; } + if (my $tpmver = $conf->{tpm}) { + my $tpmdev = print_tpm_device($vmid, $tpmver); + push @$devices, @$tpmdev; + } + my $sockets = 1; $sockets = $conf->{smp} if $conf->{smp}; # old style - no longer iused $sockets = $conf->{sockets} if $conf->{sockets}; @@ -4829,6 +4950,8 @@ sub vmconfig_apply_pending { } } + PVE::QemuServer::clear_tpm_states($vmid, $conf->{tpm}); + # write all changes at once to avoid unnecessary i/o PVE::QemuConfig->write_config($vmid, $conf); } @@ -5329,8 +5452,17 @@ sub vm_start_nolock { PVE::Tools::run_fork sub { PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties); + my $tpmpid; + if (my $tpmver = $conf->{tpm}) { + # start the TPM emulator so QEMU can connect on start + $tpmpid = start_swtpm($vmid, $tpmver, $migratedfrom); + } + my $exitcode = run_command($cmd, %run_params); - die "QEMU exited with code $exitcode\n" if $exitcode; + if ($exitcode) { + kill 'TERM', $tpmpid if $tpmpid; + die "QEMU exited with code $exitcode\n"; + } }; }; -- 2.30.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel