diff --git a/CHANGELOG.md b/CHANGELOG.md index 3477643..5beecaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) - Switch to use VM image `windows-latest` to build phase. - Use latest DscCommunity scripts and files - Changed `HyperVDsc.Common` to a buildable module. + - Added support to enable or disable the TPM on a virtual machine - Fixes [Issue #214](https://github.com/dsccommunity/HyperVDsc/issues/214). ## [3.18.0] - 2022-06-04 diff --git a/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.psm1 b/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.psm1 index 112942f..1c672e5 100644 --- a/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.psm1 +++ b/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.psm1 @@ -47,12 +47,17 @@ function Get-TargetResource } $vmSecureBootState = $false + $vmTPMState = $false if ($vmobj.Generation -eq 2) { # Retrieve secure boot status (can only be enabled on Generation 2 VMs) and convert to a boolean. $vmSecureBootState = ($vmobj | Get-VMFirmware).SecureBoot -eq 'On' + + # Retrieve TPM status (can only be enabled on Generation 2 VMs) and return boolean. + $vmTPMState = ($vmobj | Get-VMSecurity).TpmEnabled } + $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id $macAddress = @() @@ -90,6 +95,7 @@ function Get-TargetResource Path = $vmobj.Path Generation = $vmobj.Generation SecureBoot = $vmSecureBootState + EnableTPM = $vmTPMState StartupMemory = $vmobj.MemoryStartup MinimumMemory = $vmobj.MemoryMinimum MaximumMemory = $vmobj.MemoryMaximum @@ -206,6 +212,11 @@ function Set-TargetResource [System.Boolean] $SecureBoot = $true, + # Enable Trusted Platform Module for Generation 2 VMs + [Parameter()] + [System.Boolean] + $EnableTPM = $false, + # Enable Guest Services [Parameter()] [System.Boolean] @@ -427,6 +438,54 @@ function Set-TargetResource } } + if ($vmObj.Generation -eq 2) + { + # Retrive the current TPM state + $vmTPMEnabled = Test-VMTpmEnabled -Name $Name + if ($EnableTPM -ne $vmTPMEnabled) + { + Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'TPMEnabled', $EnableTPM, $vmTPMEnabled) + + # Bring the TPM state in line with the desired state (restarts the VM if needed). + if ($EnableTPM) + { + # The default value for the key protector is 0,0,0,4 + $keyProtectorDefaultValue = @(0,0,0,4) + # compare the default key protector value and the VM's key protector value + $isVMKeyProtectorDefault = -not(Compare-Object -ReferenceObject (Get-VMKeyProtector -VMName $Name) -DifferenceObject $keyProtectorDefaultValue) + + # If the VM has a default key protector, we need to create a new one before enabling the TPM + if ($isVMKeyProtectorDefault) + { + Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector + } + + $setVMPropertyParams = @{ + VMName = $Name + VMCommand = 'Enable-VMTPM' + ChangeProperty = @{ + Confirm = $false + } + RestartIfNeeded = $RestartIfNeeded + } + } + else + { + $setVMPropertyParams = @{ + VMName = $Name + VMCommand = 'Disable-VMTPM' + ChangeProperty = @{ + Confirm = $false + } + RestartIfNeeded = $RestartIfNeeded + } + } + + Set-VMProperty @setVMPropertyParams + Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'TPMEnabled', $EnableTPM) + } + } + if ($Notes -ne $null) { # If the VM notes do not match the desire notes, update them. This can be done while the VM is running. @@ -576,6 +635,26 @@ function Set-TargetResource { Set-VMFirmware -VMName $Name -EnableSecureBoot Off } + + <# + TPM is only applicable to Generation 2 VMs and it defaults to disabled. + Therefore, we only need to explicitly set it to enabled if specified. + #> + if ($EnableTPM -eq $true) + { + # The default value for the key protector is 0,0,0,4 + $keyProtectorDefaultValue = @(0,0,0,4) + # compare the default key protector value and the VM's key protector value + $isVMKeyProtectorDefault = -not(Compare-Object -ReferenceObject (Get-VMKeyProtector -VMName $Name) -DifferenceObject $keyProtectorDefaultValue) + + # If the VM has a default key protector, we need to create a new one before enabling the TPM + if ($isVMKeyProtectorDefault) + { + Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector + } + + Enable-VMTPM -VMName $Name + } } if ($EnableGuestService) @@ -687,6 +766,11 @@ function Test-TargetResource [System.Boolean] $SecureBoot = $true, + # Enable Trusted Platform Module for Generation 2 VMs + [Parameter()] + [System.Boolean] + $EnableTPM = $false, + [Parameter()] [System.Boolean] $EnableGuestService = $false, @@ -864,6 +948,13 @@ function Test-TargetResource Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot) return $false } + + $vmTPMEnabled = Test-VMTpmEnabled -Name $Name + if ($EnableTPM -ne $vmTPMEnabled) + { + Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'TPMEnabled', $EnableTPM, $vmTPMEnabled) + return $false + } } $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id @@ -988,6 +1079,18 @@ function Test-VMSecureBoot return (Get-VMFirmware -VM $vm).SecureBoot -eq 'On' } +function Test-VMTpmEnabled +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + $vm = Get-VM -Name $Name + return (Get-VMSecurity -VM $vm).TpmEnabled +} + #endregion Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof b/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof index 8416488..491d835 100644 --- a/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof +++ b/source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof @@ -17,6 +17,7 @@ class DSC_VMHyperV : OMI_BaseResource [Write, Description("Specifies if the VM should be Present (created) or Absent (removed). The default value is `Present`."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Write, Description("Notes about the VM.")] String Notes; [Write, Description("Specifies if Secure Boot should be enabled for Generation 2 virtual machines. **Only supports generation 2 virtual machines**. Default value is `$true`.")] Boolean SecureBoot; + [Write, Description("Specifies if Trusted Platform Module (TPM) should be enabled for Generation 2 virtual machines. **Only supports generation 2 virtual machines**. Default value is `$false`.")] Boolean EnableTPM; [Write, Description("Enable Guest Service Interface for the VM. The default value is `$false`.")] Boolean EnableGuestService; [Write, Description("Enable AutomaticCheckpoints for the VM.")] Boolean AutomaticCheckpointsEnabled; [Read, Description("Returns the unique ID for the VM.")] String ID; diff --git a/source/Examples/Resources/VMHyperV/7-TPMEnabled.ps1 b/source/Examples/Resources/VMHyperV/7-TPMEnabled.ps1 new file mode 100644 index 0000000..56f3ad6 --- /dev/null +++ b/source/Examples/Resources/VMHyperV/7-TPMEnabled.ps1 @@ -0,0 +1,69 @@ +<# + .SYNOPSIS + Creates a Generation 2 VM with the Trusted Platform Module (TPM) enabled. + + .DESCRIPTION + Creates a new Generation 2 virtual machine and enables the Trusted + Platform Module (TPM) on it. + + .PARAMETER NodeName + The names of one or more nodes to compile a configuration for. + Defaults to 'localhost'. + + .PARAMETER VMName + The name of the virtual machine to create. + + .PARAMETER VhdPath + The path to the VHDX file to associate with the virtual machine. + + .INPUTS + None. + + .OUTPUTS + None. + + .EXAMPLE + Example -VMName 'TPMVM' -VhdPath 'C:\VMs\TPMVM.vhdx' + + Compiles a configuration that creates a Generation 2 VM named 'TPMVM' + with TPM enabled. +#> +configuration Example +{ + param + ( + [System.String[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath + ) + + Import-DscResource -ModuleName 'HyperVDsc' + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + VMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + Generation = 2 + EnableTPM = $true + DependsOn = '[WindowsFeature]HyperV' + } + } +} diff --git a/tests/Unit/DSC_VMHyperV.Tests.ps1 b/tests/Unit/DSC_VMHyperV.Tests.ps1 index 13097b0..68ea4da 100644 --- a/tests/Unit/DSC_VMHyperV.Tests.ps1 +++ b/tests/Unit/DSC_VMHyperV.Tests.ps1 @@ -117,6 +117,35 @@ try return $stubVM } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'StoppedGeneration2VM' } -MockWith { + $stubVM = [Microsoft.HyperV.PowerShell.VirtualMachine]::CreateTypeInstance() + $stubVM.Name = 'StoppedGeneration2VM' + $stubVM.HardDrives = @( + $stubVhdxDisk, + $stubVhdDisk + ) + $stubVM.Path = $StubVMConfig.FullPath + $stubVM.Generation = 2 + $stubVM.MemoryStartup = 512MB + $stubVM.MemoryMinimum = 128MB + $stubVM.MemoryMaximum = 4096MB + $stubVM.ProcessorCount = 1 + $stubVM.ID = $mockVmGuid + $stubVM.CPUUsage = 10 + $stubVM.MemoryAssigned = 512MB + $stubVM.Uptime = New-TimeSpan -Hours 12 + $stubVM.CreationTime = (Get-Date).AddHours(-12) + $stubVM.DynamicMemoryEnabled = $true + $stubVM.Notes = '' + $stubVM.State = 'Off' + $stubVM.NetworkAdapters = @( + $stubNIC1, + $stubNIC2 + ) + + return $stubVM + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'PausedVM' } -MockWith { $stubVM = [Microsoft.HyperV.PowerShell.VirtualMachine]::CreateTypeInstance() $stubVM.Name = 'PausedVM' @@ -387,10 +416,17 @@ try It 'Calls Get-VMFirmware if a generation 2 VM' { Mock -CommandName Get-VMFirmware -MockWith { return $true } + Mock -CommandName Get-VMSecurity -MockWith { return @{ TpmEnabled = $false } } $null = Get-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk.Path Assert-MockCalled -CommandName Get-VMFirmware -Scope It -Exactly 1 } + It 'Calls Get-VMSecurity if a generation 2 VM' { + Mock -CommandName Get-VMSecurity -MockWith { return @{ TpmEnabled = $false } } + $null = Get-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk.Path + Assert-MockCalled -CommandName Get-VMSecurity -Scope It -Exactly 1 + } + It 'Hash table contains key EnableGuestService' { $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.Path $targetResource.ContainsKey('EnableGuestService') | Should -Be $true @@ -474,6 +510,7 @@ try It 'Returns $true when VM .vhdx file is specified with a generation 2 VM' { Mock -CommandName Test-VMSecureBoot -MockWith { return $true } + Mock -CommandName Test-VMTpmEnabled -MockWith { return $false } Test-TargetResource -Name 'Generation2VM' -Generation 2 @testParams | Should -Be $true } @@ -512,6 +549,20 @@ try Test-TargetResource -Name 'Generation2VM' -SecureBoot $false -Generation 2 @testParams | Should -Be $false } + It 'Returns $true when TPM is enabled and requested "EnableTPM" = "$true"' { + Mock -CommandName Test-VMTpmEnabled -MockWith { return $true } + + Test-TargetResource -Name 'Generation2VM' -EnableTPM $true -Generation 2 @testParams | + Should -BeTrue + } + + It 'Returns $false when TPM is disabled and requested "EnableTPM" = "$true"' { + Mock -CommandName Test-VMTpmEnabled -MockWith { return $false } + + Test-TargetResource -Name 'Generation2VM' -EnableTPM $true -Generation 2 @testParams | + Should -BeFalse + } + It 'Returns $true when VM has snapshot chain' { Mock -CommandName Get-VhdHierarchy -MockWith { return @($studVhdxDiskSnapshot, $stubVhdxDisk) @@ -605,6 +656,8 @@ try Mock -CommandName Get-VMNetworkAdapter -MockWith { return $stubVM.NetworkAdapters.IpAddresses } Mock -CommandName Set-VMState -MockWith { return $true } Mock -CommandName Set-VMMemory + # Default TPM state for generation 2 VMs so the new TPM code path does not hit the Hyper-V stub. + Mock -CommandName Test-VMTpmEnabled -MockWith { return $false } It 'Removes an existing VM when "Ensure" = "Absent"' { Set-TargetResource -Name 'RunningVM' -Ensure Absent @testParams @@ -778,6 +831,47 @@ try Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 1 -Scope It } + It 'Enables TPM without prompting when an existing generation 2 VM has TPM disabled' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true } + Mock -CommandName Test-VMTpmEnabled -MockWith { return $false } + Mock -CommandName Get-VMKeyProtector -MockWith { return @(0, 0, 0, 4) } + Mock -CommandName Set-VMKeyProtector + Mock -CommandName Set-VMProperty + + Set-TargetResource -Name 'StoppedGeneration2VM' -Generation 2 -EnableTPM $true @testParams + + Assert-MockCalled -CommandName Set-VMKeyProtector -Exactly 1 -Scope It + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Enable-VMTPM' -and + $ChangeProperty.Confirm -eq $false + } -Exactly 1 -Scope It + } + + It 'Disables TPM without prompting when an existing generation 2 VM has TPM enabled' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true } + Mock -CommandName Test-VMTpmEnabled -MockWith { return $true } + Mock -CommandName Set-VMProperty + + Set-TargetResource -Name 'StoppedGeneration2VM' -Generation 2 -EnableTPM $false @testParams + + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Disable-VMTPM' -and + $ChangeProperty.Confirm -eq $false + } -Exactly 1 -Scope It + } + + It 'Does not configure TPM on an existing generation 1 VM when generation 2 is requested' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true } + Mock -CommandName Test-VMTpmEnabled -MockWith { return $false } + Mock -CommandName Set-VMProperty + + Set-TargetResource -Name 'StoppedVM' -Generation 2 -EnableTPM $true @testParams + + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -in @('Enable-VMTPM', 'Disable-VMTPM') + } -Exactly 0 -Scope It + } + It 'Does call "Enable-VMIntegrationService" when "EnableGuestService" = "$true"' { Mock -CommandName Enable-VMIntegrationService Set-TargetResource -Name 'RunningVM' -EnableGuestService $true @testParams