diff --git a/CHANGELOG.md b/CHANGELOG.md index 7988ba3a..a716dceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + +### Added + +- PfxImport: + - Added Base64Content parameter to specify the content of a PFX file that can be included in the configuration MOF - Fixes [Issue #241](https://github.com/dsccommunity/CertificateDsc/issues/241). +- CertificateImport: + - Added Base64Content parameter to specify the content of a certificate file that can be included in the configuration MOF - Fixes [Issue #241](https://github.com/dsccommunity/CertificateDsc/issues/241). + ### Changed - Renamed `master` branch to `main` - Fixes [Issue #237](https://github.com/dsccommunity/CertificateDsc/issues/237). diff --git a/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.psm1 b/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.psm1 index 414c5541..650b9c3f 100644 --- a/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.psm1 +++ b/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.psm1 @@ -23,6 +23,10 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' The path to the CER file you want to import. This parameter is ignored. + .PARAMETER Content + The base64 encoded content of the CER file you want to import. + This parameter is ignored. + .PARAMETER Location The Windows Certificate Store Location to import the certificate to. @@ -48,10 +52,14 @@ function Get-TargetResource [System.String] $Thumbprint, - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $Path, + [Parameter()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -95,6 +103,7 @@ function Get-TargetResource return @{ Thumbprint = $Thumbprint Path = $Path + Content = $Content Location = $Location Store = $Store Ensure = $Ensure @@ -112,6 +121,9 @@ function Get-TargetResource .PARAMETER Path The path to the CER file you want to import. + .PARAMETER Content + The base64 encoded content of the CER file you want to import. + .PARAMETER Location The Windows Certificate Store Location to import the certificate to. @@ -135,10 +147,14 @@ function Test-TargetResource [System.String] $Thumbprint, - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $Path, + [Parameter()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -165,6 +181,8 @@ function Test-TargetResource $($script:localizedData.TestingCertificateStatusMessage -f $Thumbprint, $Location, $Store) ) -join '' ) + Assert-ResourceProperty @PSBoundParameters + $currentState = Get-TargetResource @PSBoundParameters if ($Ensure -ne $currentState.Ensure) @@ -198,6 +216,9 @@ function Test-TargetResource .PARAMETER Path The path to the CER file you want to import. + .PARAMETER Content + The base64 encoded content of the CER file you want to import. + .PARAMETER Location The Windows Certificate Store Location to import the certificate to. @@ -220,10 +241,14 @@ function Set-TargetResource [System.String] $Thumbprint, - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $Path, + [Parameter()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -250,6 +275,8 @@ function Set-TargetResource $($script:localizedData.SettingCertificateStatusMessage -f $Thumbprint, $Location, $Store) ) -join '' ) + Assert-ResourceProperty @PSBoundParameters + if ($Ensure -ieq 'Present') { $currentState = Get-TargetResource @PSBoundParameters @@ -262,14 +289,6 @@ function Set-TargetResource $($script:localizedData.ImportingCertficateMessage -f $Path, $Location, $Store) ) -join '' ) - # Check that the certificate file exists before trying to import - if (-not (Test-Path -Path $Path)) - { - New-InvalidArgumentException ` - -Message ($script:localizedData.CertificateFileNotFoundError -f $Path) ` - -ArgumentName 'Path' - } - $getCertificateStorePathParameters = @{ Location = $Location Store = $Store @@ -278,15 +297,29 @@ function Set-TargetResource $importCertificateParameters = @{ CertStoreLocation = $certificateStore - FilePath = $Path Verbose = $VerbosePreference } - <# - Using Import-CertificateEx instead of Import-Certificate due to the following issue: - https://github.com/dsccommunity/CertificateDsc/issues/161 - #> - Import-CertificateEx @importCertificateParameters + if ($PSBoundParameters.ContainsKey('Content')) + { + Import-CertificateEx @importCertificateParameters -Base64Content $Content + } + else + { + # Check that the certificate file exists before trying to import + if (-not (Test-Path -Path $Path)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.CertificateFileNotFoundError -f $Path) ` + -ArgumentName 'Path' + } + + <# + Using Import-CertificateEx instead of Import-Certificate due to the following issue: + https://github.com/dsccommunity/CertificateDsc/issues/161 + #> + Import-CertificateEx @importCertificateParameters -FilePath $Path + } } if ($PSBoundParameters.ContainsKey('FriendlyName') ` @@ -319,6 +352,46 @@ function Set-TargetResource -Location $Location ` -Store $Store } -} # end function Test-TargetResource +} # end function Set-TargetResource + +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Content, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(ValueFromRemainingArguments)] + $RemainingParameters + ) + + if ($Ensure -ieq 'Present') + { + if ([System.String]::IsNullOrWhiteSpace($Content) -band [System.String]::IsNullOrWhiteSpace($Path)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.ContentAndPathParametersAreNull) ` + -ArgumentName 'Path|Content' + } + + if ($PSBoundParameters.ContainsKey('Content') -and $PSBoundParameters.ContainsKey('Path')) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.ContentAndPathParametersAreSet) ` + -ArgumentName 'Path|Content' + } + } +} Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.schema.mof b/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.schema.mof index ee6f2995..17739561 100644 --- a/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.schema.mof +++ b/source/DSCResources/DSC_CertificateImport/DSC_CertificateImport.schema.mof @@ -2,7 +2,8 @@ class DSC_CertificateImport : OMI_BaseResource { [Key,Description("The thumbprint (unique identifier) of the certificate you're importing.")] string Thumbprint; - [Required,Description("The path to the CER file you want to import.")] string Path; + [Write,Description("The path to the CER file you want to import.")] string Path; + [Write,Description("The base64 encoded content of the CER file you want to import.")] string Content; [Key,Description("The Windows Certificate Store Location to import the certificate to."),ValueMap{"LocalMachine", "CurrentUser"},Values{"LocalMachine", "CurrentUser"}] string Location; [Key,Description("The Windows Certificate Store Name to import the certificate to.")] string Store; [Write,Description("Specifies whether the certificate should be present or absent."),ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; diff --git a/source/DSCResources/DSC_CertificateImport/en-US/DSC_CertificateImport.strings.psd1 b/source/DSCResources/DSC_CertificateImport/en-US/DSC_CertificateImport.strings.psd1 index f2b10ae8..00b3ef56 100644 --- a/source/DSCResources/DSC_CertificateImport/en-US/DSC_CertificateImport.strings.psd1 +++ b/source/DSCResources/DSC_CertificateImport/en-US/DSC_CertificateImport.strings.psd1 @@ -7,4 +7,6 @@ ConvertFrom-StringData @' CertificateFileNotFoundError = Certificate Pfx file '{0}' not found. (CI0006) SettingCertficateFriendlyNameMessage = Setting Certificate '{0}' from '{1}' store '{2}' friendly name to '{3}'. (CI0007) CertificateFriendlyNameMismatchMessage = The Fiendly Name of Certificate '{0}' from '{1}' store '{2}' is set to '{3}', but should be '{4}'. (CI0008) + ContentAndPathParametersAreNull = A non-null or non-empty value must be supplied for the Path or Content parameter. (CI0009) + ContentAndPathParametersAreSet = The use of both Path and Content parameters is not supported. (CI0010) '@ diff --git a/source/DSCResources/DSC_PfxImport/DSC_PfxImport.psm1 b/source/DSCResources/DSC_PfxImport/DSC_PfxImport.psm1 index c707ee7a..0e7c92fc 100644 --- a/source/DSCResources/DSC_PfxImport/DSC_PfxImport.psm1 +++ b/source/DSCResources/DSC_PfxImport/DSC_PfxImport.psm1 @@ -23,6 +23,10 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' The path to the PFX file you want to import. This parameter is ignored. + .PARAMETER Content + The base64 encoded content of the PFX file you want to import. + This parameter is ignored. + .PARAMETER Location The Windows Certificate Store Location to import the PFX file to. @@ -62,6 +66,11 @@ function Get-TargetResource [System.String] $Path, + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -138,6 +147,7 @@ function Get-TargetResource return @{ Thumbprint = $Thumbprint Path = $Path + Content = $Content Location = $Location Store = $Store Exportable = $Exportable @@ -157,6 +167,9 @@ function Get-TargetResource .PARAMETER Path The path to the PFX file you want to import. + .PARAMETER Content + The base64 encoded content of the PFX file you want to import. + .PARAMETER Location The Windows Certificate Store Location to import the PFX file to. @@ -192,6 +205,11 @@ function Test-TargetResource [System.String] $Path, + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -226,6 +244,8 @@ function Test-TargetResource $($script:localizedData.TestingPfxStatusMessage -f $Thumbprint, $Location, $Store) ) -join '' ) + Assert-ResourceProperty @PSBoundParameters + $currentState = Get-TargetResource @PSBoundParameters if ($Ensure -ne $currentState.Ensure) @@ -259,6 +279,9 @@ function Test-TargetResource .PARAMETER Path The path to the PFX file you want to import. + .PARAMETER Content + The base64 encoded content of the PFX file you want to import. + .PARAMETER Location The Windows Certificate Store Location to import the PFX file to. @@ -293,6 +316,11 @@ function Set-TargetResource [System.String] $Path, + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Content, + [Parameter(Mandatory = $true)] [ValidateSet('CurrentUser', 'LocalMachine')] [System.String] @@ -327,6 +355,8 @@ function Set-TargetResource $($script:localizedData.SettingPfxStatusMessage -f $Thumbprint, $Location, $Store) ) -join '' ) + Assert-ResourceProperty @PSBoundParameters + if ($Ensure -ieq 'Present') { $currentState = Get-TargetResource @PSBoundParameters @@ -339,14 +369,6 @@ function Set-TargetResource $($script:localizedData.ImportingPfxMessage -f $Path, $Location, $Store) ) -join '' ) - # Check that the certificate PFX file exists before trying to import - if (-not (Test-Path -Path $Path)) - { - New-InvalidArgumentException ` - -Message ($script:localizedData.CertificatePfxFileNotFoundError -f $Path) ` - -ArgumentName 'Path' - } - $getCertificateStorePathParameters = @{ Location = $Location Store = $Store @@ -356,7 +378,6 @@ function Set-TargetResource $importPfxCertificateParameters = @{ Exportable = $Exportable CertStoreLocation = $certificateStore - FilePath = $Path Verbose = $VerbosePreference } @@ -365,14 +386,29 @@ function Set-TargetResource $importPfxCertificateParameters['Password'] = $Credential.Password } - # If the built in PKI cmdlet exists then use that, otherwise command in Common module. - if (Test-CommandExists -Name 'Import-PfxCertificate') + if ($PSBoundParameters.ContainsKey('Content')) { - Import-PfxCertificate @importPfxCertificateParameters + Import-PfxCertificateEx @importPfxCertificateParameters -Base64Content $Content } else { - Import-PfxCertificateEx @importPfxCertificateParameters + # Check that the certificate PFX file exists before trying to import + if (-not (Test-Path -Path $Path)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.CertificatePfxFileNotFoundError -f $Path) ` + -ArgumentName 'Path' + } + + # If the built in PKI cmdlet exists then use that, otherwise command in Common module. + if (Test-CommandExists -Name 'Import-PfxCertificate') + { + Import-PfxCertificate @importPfxCertificateParameters -FilePath $Path + } + else + { + Import-PfxCertificateEx @importPfxCertificateParameters -FilePath $Path + } } } @@ -408,4 +444,44 @@ function Set-TargetResource } } # end function Set-TargetResource +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Content, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(ValueFromRemainingArguments)] + $RemainingParameters + ) + + if ($Ensure -ieq 'Present') + { + if ([System.String]::IsNullOrWhiteSpace($Content) -band [System.String]::IsNullOrWhiteSpace($Path)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.ContentAndPathParametersAreNull) ` + -ArgumentName 'Path|Content' + } + + if ($PSBoundParameters.ContainsKey('Content') -and $PSBoundParameters.ContainsKey('Path')) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.ContentAndPathParametersAreSet) ` + -ArgumentName 'Path|Content' + } + } +} + Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_PfxImport/DSC_PfxImport.schema.mof b/source/DSCResources/DSC_PfxImport/DSC_PfxImport.schema.mof index 7461f7a3..46177c61 100644 --- a/source/DSCResources/DSC_PfxImport/DSC_PfxImport.schema.mof +++ b/source/DSCResources/DSC_PfxImport/DSC_PfxImport.schema.mof @@ -3,6 +3,7 @@ class DSC_PfxImport : OMI_BaseResource { [Key,Description("The thumbprint (unique identifier) of the PFX file you're importing.")] string Thumbprint; [Write,Description("The path to the PFX file you want to import.")] string Path; + [Write,Description("The base64 encoded content of the PFX file you want to import.")] string Content; [Key,Description("The Windows Certificate Store Location to import the PFX file to."),ValueMap{"LocalMachine", "CurrentUser"},Values{"LocalMachine", "CurrentUser"}] string Location; [Key,Description("The Windows Certificate Store Name to import the PFX file to.")] string Store; [write,Description("Determines whether the private key is exportable from the machine after it has been imported")] boolean Exportable; diff --git a/source/DSCResources/DSC_PfxImport/en-US/DSC_PfxImport.strings.psd1 b/source/DSCResources/DSC_PfxImport/en-US/DSC_PfxImport.strings.psd1 index 5853d7d8..b5c7f526 100644 --- a/source/DSCResources/DSC_PfxImport/en-US/DSC_PfxImport.strings.psd1 +++ b/source/DSCResources/DSC_PfxImport/en-US/DSC_PfxImport.strings.psd1 @@ -10,4 +10,6 @@ ConvertFrom-StringData @' CertificatePfxFileNotFoundError = Certificate Pfx file '{0}' not found. (PI0010) SettingCertficateFriendlyNameMessage = Setting Certificate '{0}' from '{1}' store '{2}' friendly name to '{3}'. (PI0011) CertificateFriendlyNameMismatchMessage = The Fiendly Name of Certificate '{0}' from '{1}' store '{2}' is set to '{3}', but should be '{4}'. (PI0012) + ContentAndPathParametersAreNull = A non-null or non-empty value must be supplied for the Path or Content parameter. (CI0013) + ContentAndPathParametersAreSet = The use of both Path and Content parameters is not supported. (CI0014) '@ diff --git a/source/Examples/Resources/CertificateImport/3-CertificateImport_WithContent_Config.ps1 b/source/Examples/Resources/CertificateImport/3-CertificateImport_WithContent_Config.ps1 new file mode 100644 index 00000000..2ed2a3e6 --- /dev/null +++ b/source/Examples/Resources/CertificateImport/3-CertificateImport_WithContent_Config.ps1 @@ -0,0 +1,46 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 48a20a89-efc4-4ea7-8da5-e9f740aa89fe +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/CertificateDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/CertificateDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module CertificateDsc + +<# + .DESCRIPTION + Import public key certificate into Trusted Root store from + a provided base64 encoded string. +#> +Configuration CertificateImport_WithContent_Config +{ + Import-DscResource -ModuleName CertificateDsc + + <# + Create mock base64 value + example for converting an existing file: + $contentBase64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($certificateFilePath)) + #> + $contentBase64 = [System.Convert]::ToBase64String(@(00, 00, 00)) + + Node localhost + { + CertificateImport MyTrustedRoot + { + Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' + Location = 'LocalMachine' + Store = 'Root' + Content = $contentBase64 + } + } +} diff --git a/source/Examples/Resources/PfxImport/6-PfxImport_InstallPFXFromContent_Config.ps1 b/source/Examples/Resources/PfxImport/6-PfxImport_InstallPFXFromContent_Config.ps1 new file mode 100644 index 00000000..86eb71b9 --- /dev/null +++ b/source/Examples/Resources/PfxImport/6-PfxImport_InstallPFXFromContent_Config.ps1 @@ -0,0 +1,54 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID fa81342d-b96d-401b-8a18-c96bebd4aff6 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/CertificateDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/CertificateDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -Modules CertificateDsc + +<# + .DESCRIPTION + Import a PFX into the 'My' Local Machine certificate store. +#> +Configuration PfxImport_InstallPFXFromContent_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName CertificateDsc + + <# + Create mock base64 value + example for converting an existing file: + $contentBase64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($certificateFilePath)) + #> + $contentBase64 = [System.Convert]::ToBase64String(@(00, 00, 00)) + + Node localhost + { + PfxImport CompanyCert + { + Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' + Content = $contentBase64 + Location = 'LocalMachine' + Store = 'My' + Credential = $Credential + } + } +} diff --git a/source/Modules/CertificateDsc.Common/CertificateDsc.Common.psm1 b/source/Modules/CertificateDsc.Common/CertificateDsc.Common.psm1 index 2ca06627..3febd6b1 100644 --- a/source/Modules/CertificateDsc.Common/CertificateDsc.Common.psm1 +++ b/source/Modules/CertificateDsc.Common/CertificateDsc.Common.psm1 @@ -952,6 +952,9 @@ function Test-CommandExists .PARAMETER FilePath The path to the certificate file to import. + .PARAMETER Base64Content + The base64 content of the certificate file to import. + .PARAMETER CertStoreLocation The Certificate Store and Location Path to import the certificate to. #> @@ -960,10 +963,14 @@ function Import-CertificateEx [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = 'Path', Mandatory = $true)] [System.String] $FilePath, + [Parameter(ParameterSetName = 'Content', Mandatory = $true)] + [System.String] + $Base64Content, + [Parameter(Mandatory = $true)] [System.String] $CertStoreLocation @@ -973,8 +980,17 @@ function Import-CertificateEx $store = Split-Path -Path $CertStoreLocation -Leaf $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection - $cert.Import($FilePath) + if ($PSCmdlet.ParameterSetName -eq 'Path') + { + $certificateData = $FilePath + } + else + { + $certificateData = [Convert]::FromBase64String($Base64Content) + } + + $cert.Import($certificateData) $certStore = New-Object ` -TypeName System.Security.Cryptography.X509Certificates.X509Store ` -ArgumentList ($store, $location) @@ -992,6 +1008,9 @@ function Import-CertificateEx .PARAMETER FilePath The path to the certificate file to import. + .PARAMETER Base64Content + The base64 content of the certificate file to import. + .PARAMETER CertStoreLocation The Certificate Store and Location Path to import the certificate to. @@ -1006,10 +1025,14 @@ function Import-PfxCertificateEx [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = 'Path', Mandatory = $true)] [System.String] $FilePath, + [Parameter(ParameterSetName = 'Content', Mandatory = $true)] + [System.String] + $Base64Content, + [Parameter(Mandatory = $true)] [System.String] $CertStoreLocation, @@ -1030,6 +1053,15 @@ function Import-PfxCertificateEx $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet + if ($PSCmdlet.ParameterSetName -eq 'Path') + { + $importDataValue = $FilePath + } + else + { + $importDataValue = [Convert]::FromBase64String($Base64Content) + } + if ($Exportable) { $flags = $flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable @@ -1037,11 +1069,11 @@ function Import-PfxCertificateEx if ($Password) { - $cert.Import($FilePath, $Password, $flags) + $cert.Import($importDataValue, $Password, $flags) } else { - $cert.Import($FilePath, $flags) + $cert.Import($importDataValue, $flags) } $certStore = New-Object ` diff --git a/tests/Integration/DSC_CertificateImport.Integration.Tests.ps1 b/tests/Integration/DSC_CertificateImport.Integration.Tests.ps1 index a07124a3..839e826a 100644 --- a/tests/Integration/DSC_CertificateImport.Integration.Tests.ps1 +++ b/tests/Integration/DSC_CertificateImport.Integration.Tests.ps1 @@ -45,21 +45,30 @@ try -Path $certificate.PSPath ` -Force $certificateFriendlyName = 'Test Certificate Friendly Name' + + $testBase64Content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($certificatePath)) } AfterAll { # Cleanup - $null = Remove-Item ` - -Path $certificatePath ` - -Force ` - -ErrorAction SilentlyContinue - $null = Remove-Item ` - -Path $certificate.PSPath ` - -Force ` - -ErrorAction SilentlyContinue + if ($null -ne $certificatePath) + { + $null = Remove-Item ` + -Path $certificatePath ` + -Force ` + -ErrorAction SilentlyContinue + } + + if ($null -ne $certificate.PSPath) + { + $null = Remove-Item ` + -Path $certificate.PSPath ` + -Force ` + -ErrorAction SilentlyContinue + } } - Context 'Import certificate' { + Context 'Import certificate file' { $configData = @{ AllNodes = @( @{ @@ -109,6 +118,56 @@ try } } + Context 'Import certificate content' { + $configData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + Thumbprint = $certificate.Thumbprint + Location = 'LocalMachine' + Store = 'My' + Ensure = 'Present' + Content = $testBase64Content + FriendlyName = $certificateFriendlyName + } + ) + } + + It 'Should compile the MOF without throwing an exception' { + { + & "$($script:DSCResourceName)_Config_WithContent" ` + -OutputPath $TestDrive ` + -ConfigurationData $configData + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing an exception' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + # Get the Certificate details + $certificateNew = Get-Item ` + -Path "Cert:\LocalMachine\My\$($certificate.Thumbprint)" + $certificateNew | Should -BeOfType System.Security.Cryptography.X509Certificates.X509Certificate2 + $certificateNew.Thumbprint | Should -BeExactly $certificate.Thumbprint + $certificateNew.Subject | Should -BeExactly $certificate.Subject + $certificateNew.FriendlyName | Should -BeExactly $certificateFriendlyName + } + } + Context 'Remove certificate' { $configData = @{ AllNodes = @( diff --git a/tests/Integration/DSC_CertificateImport.config.ps1 b/tests/Integration/DSC_CertificateImport.config.ps1 index d9e4aacf..dd899518 100644 --- a/tests/Integration/DSC_CertificateImport.config.ps1 +++ b/tests/Integration/DSC_CertificateImport.config.ps1 @@ -12,3 +12,18 @@ Configuration DSC_CertificateImport_Config { } } } + +Configuration DSC_CertificateImport_Config_WithContent { + Import-DscResource -ModuleName CertificateDsc + + node localhost { + CertificateImport Integration_Test { + Thumbprint = $Node.Thumbprint + Content = $Node.Content + Location = $Node.Location + Store = $Node.Store + Ensure = $Node.Ensure + FriendlyName = $Node.FriendlyName + } + } +} diff --git a/tests/Integration/DSC_PfxImport.Integration.Tests.ps1 b/tests/Integration/DSC_PfxImport.Integration.Tests.ps1 index b19717e1..e3f71216 100644 --- a/tests/Integration/DSC_PfxImport.Integration.Tests.ps1 +++ b/tests/Integration/DSC_PfxImport.Integration.Tests.ps1 @@ -40,6 +40,11 @@ try $pfxPath = Join-Path ` -Path $env:Temp ` -ChildPath "PfxImport-$($certificate.Thumbprint).pfx" + + $pfxPathContentOutput = Join-Path ` + -Path $env:Temp ` + -ChildPath "PfxContentImport1-$($certificate.Thumbprint).pfx" + $cerPath = Join-Path ` -Path $env:Temp ` -ChildPath "CerImport-$($certificate.Thumbprint).cer" @@ -55,6 +60,8 @@ try -FilePath $pfxPath ` -Password $testCredential.Password + $testBase64Content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($pfxPath)) + $null = Export-Certificate ` -Type CERT ` -Cert $certificate ` @@ -82,6 +89,22 @@ try ) } + $configDataForAddWithContent = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + Thumbprint = $certificate.Thumbprint + Location = 'LocalMachine' + Store = 'My' + Ensure = 'Present' + Content = $testBase64Content + Credential = $testCredential + FriendlyName = $certificateFriendlyName + PSDscAllowPlainTextPassword = $true + } + ) + } + $configDataForRemove = @{ AllNodes = @( @{ @@ -102,13 +125,17 @@ try -Path $pfxPath ` -Force ` -ErrorAction SilentlyContinue + $null = Remove-Item ` + -Path $pfxPathContentOutput ` + -Force ` + -ErrorAction SilentlyContinue $null = Remove-Item ` -Path $certificate.PSPath ` -Force ` -ErrorAction SilentlyContinue } - Context 'When certificate has not been imported yet' { + Context 'When certificate file has not been imported yet' { It 'Should compile the MOF without throwing an exception' { { & "$($script:DSCResourceName)_Add_Config" ` @@ -145,7 +172,7 @@ try } } - Context 'When certificate has been imported but the private key is missing' { + Context 'When certificate file has been imported but the private key is missing' { $null = Remove-Item ` -Path $certificate.PSPath ` -Force @@ -188,7 +215,7 @@ try } } - Context 'When certificate has already been imported' { + Context 'When certificate file has already been imported' { It 'Should compile the MOF without throwing an exception' { { & "$($script:DSCResourceName)_Add_Config" ` @@ -225,6 +252,123 @@ try } } + Context 'When certificate content has not been imported yet' { + It 'Should compile the MOF without throwing an exception' { + { + & "$($script:DSCResourceName)_Add_Config_With_Content" ` + -OutputPath $TestDrive ` + -ConfigurationData $configDataForAddWithContent + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing an exception' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + # Get the Certificate details + $certificateNew = Get-Item ` + -Path "Cert:\LocalMachine\My\$($certificate.Thumbprint)" + $certificateNew | Should -BeOfType System.Security.Cryptography.X509Certificates.X509Certificate2 + $certificateNew.HasPrivateKey | Should -Be $true + $certificateNew.Thumbprint | Should -BeExactly $certificate.Thumbprint + $certificateNew.Subject | Should -BeExactly $certificate.Subject + $certificateNew.FriendlyName | Should -BeExactly $certificateFriendlyName + } + } + + Context 'When certificate content has been imported but the private key is missing' { + $null = Remove-Item ` + -Path $certificate.PSPath ` + -Force + + Import-Certificate -FilePath $cerPath -CertStoreLocation Cert:\LocalMachine\My + + It 'Should compile the MOF without throwing an exception' { + { + & "$($script:DSCResourceName)_Add_Config_With_Content" ` + -OutputPath $TestDrive ` + -ConfigurationData $configDataForAddWithContent + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing an exception' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + # Get the Certificate details + $certificateNew = Get-Item ` + -Path "Cert:\LocalMachine\My\$($certificate.Thumbprint)" + $certificateNew | Should -BeOfType System.Security.Cryptography.X509Certificates.X509Certificate2 + $certificateNew.HasPrivateKey | Should -BeTrue + $certificateNew.Thumbprint | Should -BeExactly $certificate.Thumbprint + $certificateNew.Subject | Should -BeExactly $certificate.Subject + $certificateNew.FriendlyName | Should -BeExactly $certificateFriendlyName + } + } + + Context 'When certificate content has already been imported' { + It 'Should compile the MOF without throwing an exception' { + { + & "$($script:DSCResourceName)_Add_Config_With_Content" ` + -OutputPath $TestDrive ` + -ConfigurationData $configDataForAddWithContent + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing an exception' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + # Get the Certificate details + $certificateNew = Get-Item ` + -Path "Cert:\LocalMachine\My\$($certificate.Thumbprint)" + $certificateNew | Should -BeOfType System.Security.Cryptography.X509Certificates.X509Certificate2 + $certificateNew.HasPrivateKey | Should -BeTrue + $certificateNew.Thumbprint | Should -BeExactly $certificate.Thumbprint + $certificateNew.Subject | Should -BeExactly $certificate.Subject + $certificateNew.FriendlyName | Should -BeExactly $certificateFriendlyName + } + } + Context 'When certificate has been imported but needs to be removed' { It 'Should compile the MOF without throwing an exception' { { diff --git a/tests/Integration/DSC_PfxImport.config.ps1 b/tests/Integration/DSC_PfxImport.config.ps1 index 0b3720ff..0faea09e 100644 --- a/tests/Integration/DSC_PfxImport.config.ps1 +++ b/tests/Integration/DSC_PfxImport.config.ps1 @@ -14,6 +14,22 @@ Configuration DSC_PfxImport_Add_Config { } } +Configuration DSC_PfxImport_Add_Config_With_Content { + Import-DscResource -ModuleName CertificateDsc + + node localhost { + PfxImport Integration_Test { + Thumbprint = $Node.Thumbprint + Content = $Node.Content + Location = $Node.Location + Store = $Node.Store + Ensure = $Node.Ensure + Credential = $Node.Credential + FriendlyName = $Node.FriendlyName + } + } +} + Configuration DSC_PfxImport_Remove_Config { Import-DscResource -ModuleName CertificateDsc diff --git a/tests/Unit/DSC_CertificateImport.Tests.ps1 b/tests/Unit/DSC_CertificateImport.Tests.ps1 index 672ce827..9ecd0a6c 100644 --- a/tests/Unit/DSC_CertificateImport.Tests.ps1 +++ b/tests/Unit/DSC_CertificateImport.Tests.ps1 @@ -37,6 +37,8 @@ try $testFile = 'test.cer' $certificateFriendlyName = 'Test Certificate Friendly Name' + $certificateContent = [System.Convert]::ToBase64String(@(00, 00, 00)) + $validPath = "TestDrive:\$testFile" $validCertPath = "Cert:\LocalMachine\My" @@ -64,11 +66,16 @@ try $Store -eq 'My' } - $importCertificateEx_parameterfilter = { + $importCertificateExWithFile_parameterfilter = { $CertStoreLocation -eq $validCertPath -and ` $FilePath -eq $validPath } + $importCertificateExWithContent_parameterfilter = { + $CertStoreLocation -eq $validCertPath -and ` + $Base64Content -eq $certificateContent + } + $setCertificateFriendlyNameInCertificateStore_parameterfilter = { $Thumbprint -eq $validThumbprint -and ` $Location -eq 'LocalMachine' -and ` @@ -91,6 +98,15 @@ try Verbose = $true } + $presentParamsWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + $presentParamsWithFriendlyName = @{ Thumbprint = $validThumbprint Path = $validPath @@ -101,6 +117,42 @@ try FriendlyName = $certificateFriendlyName } + $presentParamsWithFriendlyNameWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + FriendlyName = $certificateFriendlyName + } + + $presentParamsWithoutContentAndPath = @{ + Thumbprint = $validThumbprint + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + + $presentParamsWithBothContentAndPath = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Path = $validPath + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + + $absentParamsWithBothContentAndPath = @{ + Thumbprint = $validThumbprint + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + $absentParams = @{ Thumbprint = $validThumbprint Path = $validPath @@ -110,6 +162,23 @@ try Verbose = $true } + $absentParamsWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + + $absentParamsWithoutContentAndPath = @{ + Thumbprint = $validThumbprint + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Verbose = $true + } + Describe 'DSC_CertificateImport\Get-TargetResource' -Tag 'Get' { Context 'When the certificate exists' { Mock -CommandName Get-CertificateFromCertificateStore ` @@ -145,7 +214,7 @@ try It 'Should not throw exception' { { - $script:result = Get-TargetResource @presentParams + $script:result = Get-TargetResource @presentParamsWithContent } | Should -Not -Throw } @@ -155,7 +224,8 @@ try It 'Should contain the input values' { $script:result.Thumbprint | Should -BeExactly $validThumbprint - $script:result.Path | Should -BeExactly $validPath + $script:result.Path | Should -BeExactly '' + $script:result.Content | Should -BeExactly $presentParamsWithContent.Content $script:result.Ensure | Should -BeExactly 'Absent' $script:result.FriendlyName | Should -BeNullOrEmpty } @@ -170,6 +240,34 @@ try } Describe 'DSC_CertificateImport\Test-TargetResource' -Tag 'Test' { + Context 'When Content and Path parameters are null' { + It 'Should throw exception when Ensure is Present' { + { + Test-TargetResource @presentParamsWithoutContentAndPath + } | Should -Throw + } + + It 'Should not throw exception when Ensure is Absent' { + { + Test-TargetResource @absentParamsWithoutContentAndPath + } | Should -Not -Throw + } + } + + Context 'When both Content and Path parameters are set' { + It 'Should throw exception when Ensure is Present' { + { + Test-TargetResource @presentParamsWithBothContentAndPath + } | Should -Throw ($script:localizedData.ContentAndPathParametersAreSet) + } + + It 'Should not throw exception when Ensure is Absent' { + { + Test-TargetResource @absentParamsWithBothContentAndPath + } | Should -Not -Throw + } + } + Context 'When certificate is not in store but should be' { Mock -CommandName Get-CertificateFromCertificateStore @@ -231,6 +329,34 @@ try Mock -CommandName Set-CertificateFriendlyNameInCertificateStore } + Context 'When Content and Path parameters are null' { + It 'Should throw exception when Ensure is Present' { + { + Set-TargetResource @presentParamsWithoutContentAndPath + } | Should -Throw + } + + It 'Should not throw exception when Ensure is Absent' { + { + Set-TargetResource @absentParamsWithoutContentAndPath + } | Should -Not -Throw + } + } + + Context 'When both Content and Path parameters are set' { + It 'Should throw exception when Ensure is Present' { + { + Set-TargetResource @presentParamsWithBothContentAndPath + } | Should -Throw ($script:localizedData.ContentAndPathParametersAreSet) + } + + It 'Should not throw exception when Ensure is Absent' { + { + Set-TargetResource @absentParamsWithBothContentAndPath + } | Should -Not -Throw + } + } + Context 'When certificate file exists and certificate should be in the store but is not' { Mock -CommandName Get-CertificateFromCertificateStore @@ -250,7 +376,36 @@ try It 'Should call Import-Certificate with expected parameters' { Assert-MockCalled ` -CommandName Import-CertificateEx ` - -ParameterFilter $importCertificateEx_parameterfilter ` + -ParameterFilter $importCertificateExWithFile_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When certificate content is used and certificate should be in the store but is not' { + Mock -CommandName Get-CertificateFromCertificateStore + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Not -Throw + } + + It 'Should call Test-Path with expected parameters' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should call Import-Certificate with expected parameters' { + Assert-MockCalled ` + -CommandName Import-CertificateEx ` + -ParameterFilter $importCertificateExWithContent_parameterfilter ` -Exactly -Times 1 } @@ -290,6 +445,33 @@ try } } + Context 'When certificate content is used and certificate should be in the store and is' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificate_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-Certificate' { + Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + Context 'When certificate file exists and certificate should be in the store and is and the friendly name is different' { Mock -CommandName Get-CertificateFromCertificateStore ` -MockWith $validCertificateWithDifferentFriendlyName_mock @@ -300,11 +482,41 @@ try } | Should -Not -Throw } - It 'Should not call Test-Path with the parameters supplied' { + It 'Should not call Test-Path' { Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } - It 'Should not call Import-Certificate with the parameters supplied' { + It 'Should not call Import-Certificate' { + Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 + } + + It 'Should call Set-CertificateFriendlyNameInCertificateStore with expected parameters' { + Assert-MockCalled ` + -CommandName Set-CertificateFriendlyNameInCertificateStore ` + -ParameterFilter $setCertificateFriendlyNameInCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When certificate content is used and certificate should be in the store and is and the friendly name is different' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificateWithDifferentFriendlyName_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithFriendlyNameWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-Certificate' { Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 } @@ -330,11 +542,38 @@ try } | Should -Not -Throw } - It 'Should not call Test-Path with the parameters supplied' { + It 'Should not call Test-Path' { Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } - It 'Should not call Import-Certificate with the parameters supplied' { + It 'Should not call Import-Certificate' { + Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When certificate content is used and certificate should be in the store and is and the friendly name is the same' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificate_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithFriendlyNameWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-Certificate' { Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 } @@ -377,6 +616,36 @@ try } } + Context 'When certificate content is used and certificate should not be in the store but is' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificate_mock + + It 'Should not throw exception' { + { + Set-TargetResource @absentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-CertificateEx' { + Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should call Remove-CertificateFromCertificateStore with expected parameters' { + Assert-MockCalled ` + -CommandName Remove-CertificateFromCertificateStore ` + -ParameterFilter $removeCertificateFromCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + } + Context 'When certificate file exists and certificate should not be in the store and is not' { Mock -CommandName Get-CertificateFromCertificateStore @@ -406,6 +675,35 @@ try } } + Context 'When certificate content is used and certificate should not be in the store and is not' { + Mock -CommandName Get-CertificateFromCertificateStore + + It 'Should not throw exception' { + { + Set-TargetResource @absentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-CertificateEx' { + Assert-MockCalled -CommandName Import-CertificateEx -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should call Remove-CertificateFromCertificateStore' { + Assert-MockCalled ` + -CommandName Remove-CertificateFromCertificateStore ` + -ParameterFilter $removeCertificateFromCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + } + Context 'When certificate file does not exist and certificate should be in the store' { Mock -CommandName Test-Path -MockWith { $false } @@ -434,6 +732,35 @@ try Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 } } + + Context 'When certificate content is invalid and certificate should be in the store' { + Mock -CommandName Import-CertificateEx -MockWith { throw } + + It 'Should throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should call Import-Certificate with expected parameters' { + Assert-MockCalled ` + -CommandName Import-CertificateEx ` + -ParameterFilter $importCertificateExWithContent_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } } } } diff --git a/tests/Unit/DSC_PfxImport.Tests.ps1 b/tests/Unit/DSC_PfxImport.Tests.ps1 index 969c2aa7..bfaa674a 100644 --- a/tests/Unit/DSC_PfxImport.Tests.ps1 +++ b/tests/Unit/DSC_PfxImport.Tests.ps1 @@ -44,6 +44,8 @@ try -TypeName System.Management.Automation.PSCredential ` -ArgumentList $testUsername, (ConvertTo-SecureString $testPassword -AsPlainText -Force) + $certificateContent = [System.Convert]::ToBase64String(@(00, 00, 00)) + $validPath = "TestDrive:\$testFile" $validCertPath = "Cert:\LocalMachine\My" $validCertFullPath = '{0}\{1}' -f $validCertPath, $validThumbprint @@ -84,7 +86,14 @@ try $Store -eq 'My' } - $importPfxCertificate_parameterfilter = { + $importPfxCertificateWithContent_parameterfilter = { + $CertStoreLocation -eq $validCertPath -and ` + $Base64Content -eq $certificateContent -and ` + $Exportable -eq $True -and ` + $Password -eq $testCredential.Password + } + + $importPfxCertificateWithFile_parameterfilter = { $CertStoreLocation -eq $validCertPath -and ` $FilePath -eq $validPath -and ` $Exportable -eq $True -and ` @@ -115,6 +124,17 @@ try Verbose = $True } + $presentParamsWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Exportable = $True + Credential = $testCredential + Verbose = $True + } + $presentParamsWithFriendlyName = @{ Thumbprint = $validThumbprint Path = $validPath @@ -127,6 +147,55 @@ try FriendlyName = $certificateFriendlyName } + $presentParamsWithFriendlyNameWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Exportable = $True + Credential = $testCredential + Verbose = $True + FriendlyName = $certificateFriendlyName + } + + $presentParamsWithoutContentAndPath = @{ + Thumbprint = $validThumbprint + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Exportable = $True + Credential = $testCredential + Verbose = $True + FriendlyName = $certificateFriendlyName + } + + $presentParamsWithBothContentAndPath = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Path = $validPath + Ensure = 'Present' + Location = 'LocalMachine' + Store = 'My' + Exportable = $True + Credential = $testCredential + Verbose = $True + FriendlyName = $certificateFriendlyName + } + + $absentParamsWithBothContentAndPath = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Path = $validPath + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Exportable = $True + Credential = $testCredential + Verbose = $True + FriendlyName = $certificateFriendlyName + } + $absentParams = @{ Thumbprint = $validThumbprint Ensure = 'Absent' @@ -135,6 +204,23 @@ try Verbose = $True } + $absentParamsWithContent = @{ + Thumbprint = $validThumbprint + Content = $certificateContent + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Verbose = $True + } + + $absentParamsWithoutContentAndPath = @{ + Thumbprint = $validThumbprint + Ensure = 'Absent' + Location = 'LocalMachine' + Store = 'My' + Verbose = $True + } + Describe 'DSC_PfxImport\Get-TargetResource' -Tag 'Get' { Context 'When the certificate exists with a private key' { Mock -CommandName Get-CertificateFromCertificateStore ` @@ -199,7 +285,7 @@ try It 'Should not throw exception' { { - $script:result = Get-TargetResource @presentParams + $script:result = Get-TargetResource @presentParamsWithContent } | Should -Not -Throw } @@ -209,7 +295,8 @@ try It 'Should contain the input values' { $script:result.Thumbprint | Should -BeExactly $validThumbprint - $script:result.Path | Should -BeExactly $validPath + $script:result.Path | Should -BeExactly '' + $script:result.Content | Should -Be $presentParamsWithContent.Content $script:result.Ensure | Should -BeExactly 'Absent' $script:result.FriendlyName | Should -BeNullOrEmpty } @@ -224,6 +311,34 @@ try } Describe 'DSC_PfxImport\Test-TargetResource' -Tag 'Test' { + Context 'When Content and Path parameters are null' { + It 'Should throw exception when Ensure is Present' { + { + Test-TargetResource @presentParamsWithoutContentAndPath + } | Should -Throw + } + + It 'Should not throw exception when Ensure is Absent' { + { + Test-TargetResource @absentParamsWithoutContentAndPath + } | Should -Not -Throw + } + } + + Context 'When both Content and Path parameters are set' { + It 'Should throw exception when Ensure is Present' { + { + Test-TargetResource @presentParamsWithBothContentAndPath + } | Should -Throw ($script:localizedData.ContentAndPathParametersAreSet) + } + + It 'Should not throw exception when Ensure is Absent' { + { + Test-TargetResource @absentParamsWithBothContentAndPath + } | Should -Not -Throw + } + } + Context 'When certificate is not in store but should be' { Mock -CommandName Get-CertificateFromCertificateStore @@ -281,10 +396,39 @@ try BeforeAll { Mock -CommandName Test-Path -MockWith { $true } Mock -CommandName Import-PfxCertificate + Mock -CommandName Import-PfxCertificateEx Mock -CommandName Remove-CertificateFromCertificateStore Mock -CommandName Set-CertificateFriendlyNameInCertificateStore } + Context 'When Content and Path parameters are null' { + It 'Should throw exception when Ensure is Present' { + { + Set-TargetResource @presentParamsWithoutContentAndPath + } | Should -Throw ($script:localizedData.ContentAndPathParametersAreNull) + } + + It 'Should not throw exception when Ensure is Absent' { + { + Set-TargetResource @absentParamsWithoutContentAndPath + } | Should -Not -Throw + } + } + + Context 'When both Content and Path parameters are set' { + It 'Should throw exception when Ensure is Present' { + { + Set-TargetResource @presentParamsWithBothContentAndPath + } | Should -Throw ($script:localizedData.ContentAndPathParametersAreSet) + } + + It 'Should not throw exception when Ensure is Absent' { + { + Set-TargetResource @absentParamsWithBothContentAndPath + } | Should -Not -Throw + } + } + Context 'When PFX file exists and certificate should be in the store but is not' { Mock -CommandName Get-CertificateFromCertificateStore @@ -304,10 +448,84 @@ try It 'Should call Import-PfxCertificate with expected parameters' { Assert-MockCalled ` -CommandName Import-PfxCertificate ` - -ParameterFilter $importPfxCertificate_parameterfilter ` + -ParameterFilter $importPfxCertificateWithFile_parameterfilter ` -Exactly -Times 1 } + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When PFX file exists and certificate should be in the store but is not (No Import-PfxCertificate cmdlet)' { + Mock -CommandName Get-CertificateFromCertificateStore + Mock -CommandName Test-CommandExists -MockWith { $false } + + It 'Should not throw exception' { + { + Set-TargetResource @presentParams + } | Should -Not -Throw + } + + It 'Should call Test-Path with expected parameters' { + Assert-MockCalled ` + -CommandName Test-Path ` + -ParameterFilter $testPath_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should call Import-PfxCertificateEx with expected parameters' { + Assert-MockCalled ` + -CommandName Import-PfxCertificateEx ` + -ParameterFilter $importPfxCertificateWithFile_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When PFX content is used and certificate should be in the store but is not' { + Mock -CommandName Get-CertificateFromCertificateStore + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Not -Throw + } + + It 'Should call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should call Import-PfxCertificate with expected parameters' { + Assert-MockCalled ` + -CommandName Import-PfxCertificateEx ` + -ParameterFilter $importPfxCertificateWithContent_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 } @@ -327,11 +545,42 @@ try } | Should -Not -Throw } - It 'Should not call Test-Path with expected parameters' { + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When PFX content is used and certificate should be in the store and is' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificateWithPrivateKey_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } - It 'Should not call Import-PfxCertificate with expected parameters' { + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 } @@ -374,6 +623,40 @@ try } } + Context 'When PFX content is used and certificate should be in the store and is and the friendly name is different' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificateWithDifferentFriendlyName_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithFriendlyNameWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should call Set-CertificateFriendlyNameInCertificateStore with expected parameters' { + Assert-MockCalled ` + -CommandName Set-CertificateFriendlyNameInCertificateStore ` + -ParameterFilter $setCertificateFriendlyNameInCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + Context 'When PFX file exists and certificate should be in the store and is and the friendly name is the same' { Mock -CommandName Get-CertificateFromCertificateStore ` -MockWith $validCertificateWithPrivateKey_mock @@ -384,11 +667,15 @@ try } | Should -Not -Throw } - It 'Should not call Test-Path with the parameters supplied' { + It 'Should not call Test-Path' { Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } - It 'Should not call Import-PfxCertificate with the parameters supplied' { + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 } @@ -401,6 +688,36 @@ try } } + Context 'When PFX content is used and certificate should be in the store and is and the friendly name is the same' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificateWithPrivateKey_mock + + It 'Should not throw exception' { + { + Set-TargetResource @presentParamsWithFriendlyNameWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } Context 'When PFX file exists and certificate should not be in the store but is' { Mock -CommandName Get-CertificateFromCertificateStore ` @@ -416,6 +733,44 @@ try Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should call Remove-CertificateFromCertificateStore with expected parameters' { + Assert-MockCalled ` + -CommandName Remove-CertificateFromCertificateStore ` + -ParameterFilter $removeCertificateFromCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + } + + Context 'When PFX content is used and certificate should not be in the store but is' { + Mock -CommandName Get-CertificateFromCertificateStore ` + -MockWith $validCertificateWithPrivateKey_mock + + It 'Should not throw exception' { + { + Set-TargetResource @absentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + It 'Should not call Import-PfxCertificate' { Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 } @@ -445,6 +800,43 @@ try Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 } + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should call Remove-CertificateFromCertificateStore' { + Assert-MockCalled ` + -CommandName Remove-CertificateFromCertificateStore ` + -ParameterFilter $removeCertificateFromCertificateStore_parameterfilter ` + -Exactly -Times 1 + } + } + + Context 'When PFX content is used and certificate should not be in the store and is not' { + Mock -CommandName Get-CertificateFromCertificateStore + + It 'Should not throw exception' { + { + Set-TargetResource @absentParamsWithContent + } | Should -Not -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + It 'Should not call Import-PfxCertificate' { Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 } @@ -479,6 +871,43 @@ try -Exactly -Times 1 } + It 'Should not call Import-PfxCertificateEx' { + Assert-MockCalled -CommandName Import-PfxCertificateEx -Exactly -Times 0 + } + + It 'Should not call Import-PfxCertificate' { + Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 + } + + It 'Should not call Set-CertificateFriendlyNameInCertificateStore' { + Assert-MockCalled -CommandName Set-CertificateFriendlyNameInCertificateStore -Exactly -Times 0 + } + + It 'Should not call Remove-CertificateFromCertificateStore' { + Assert-MockCalled -CommandName Remove-CertificateFromCertificateStore -Exactly -Times 0 + } + } + + Context 'When PFX content is not valid and certificate should be in the store' { + Mock -CommandName Import-PfxCertificateEx -MockWith { throw } + + It 'Should throw exception' { + { + Set-TargetResource @presentParamsWithContent + } | Should -Throw + } + + It 'Should not call Test-Path' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + } + + It 'Should call Import-PfxCertificateEx with expected parameters' { + Assert-MockCalled ` + -CommandName Import-PfxCertificateEx ` + -ParameterFilter $importPfxCertificateWithContent_parameterfilter ` + -Exactly -Times 1 + } + It 'Should not call Import-PfxCertificate' { Assert-MockCalled -CommandName Import-PfxCertificate -Exactly -Times 0 }