diff --git a/.MetaTestOptIn.json b/.MetaTestOptIn.json new file mode 100644 index 00000000..a74f3590 --- /dev/null +++ b/.MetaTestOptIn.json @@ -0,0 +1,3 @@ +[ + "Common Tests - Validate Markdown Files" +] diff --git a/DSCResources/CertificateCommon/en-us/CertificateCommon.strings.psd1 b/DSCResources/CertificateCommon/en-us/CertificateCommon.strings.psd1 deleted file mode 100644 index e8d23c6c..00000000 --- a/DSCResources/CertificateCommon/en-us/CertificateCommon.strings.psd1 +++ /dev/null @@ -1,6 +0,0 @@ -ConvertFrom-StringData @' - FileNotFoundError = File '{0}' not found. - InvalidHashError = '{0}' is not a valid hash. - - ValidHashMessage = '{0}' is a valid {1} hash. -'@ diff --git a/DSCResources/MSFT_xCertReq/MSFT_xCertReq.psm1 b/DSCResources/MSFT_xCertReq/MSFT_xCertReq.psm1 index 88d848c5..f61b4a84 100644 --- a/DSCResources/MSFT_xCertReq/MSFT_xCertReq.psm1 +++ b/DSCResources/MSFT_xCertReq/MSFT_xCertReq.psm1 @@ -1,27 +1,14 @@ #Requires -Version 4.0 -#region localizeddata -if (Test-Path "${PSScriptRoot}\${PSUICulture}") -{ - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xCertReq.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\${PSUICulture}" -} -else -{ - #fallback to en-US - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xCertReq.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\en-US" -} -#endregion - -# Import the common certificate functions -Import-Module -Name ( Join-Path ` - -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'CertificateCommon\CertificateCommon.psm1' ) +$script:ResourceRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) + +# Import the xCertificate Resource Module (to import the common modules) +Import-Module -Name (Join-Path -Path $script:ResourceRootPath -ChildPath 'xCertificate.psd1') + +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'MSFT_xCertReq' ` + -ResourcePath (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) <# .SYNOPSIS @@ -323,7 +310,7 @@ function Set-TargetResource # A unique identifier for temporary files that will be used when interacting with the command line utility $guid = [system.guid]::NewGuid().guid - $workingPath = Join-Path -Path $ENV:Temp -ChildPath "xCertReq-$guid" + $workingPath = Join-Path -Path $env:Temp -ChildPath "xCertReq-$guid" $infPath = [System.IO.Path]::ChangeExtension($workingPath,'.inf') $reqPath = [System.IO.Path]::ChangeExtension($workingPath,'.req') $cerPath = [System.IO.Path]::ChangeExtension($workingPath,'.cer') @@ -388,7 +375,7 @@ RenewalCert = $Thumbprint # SUBMIT: Submit a request to a Certification Authority. # DSC runs in the context of LocalSystem, which uses the Computer account in Active Directory # to authenticate to network resources - # The Credential paramter with xPDT is used to impersonate a user making the request + # The Credential paramter with PDT is used to impersonate a user making the request if (Test-Path -Path $reqPath) { Write-Verbose -Message ( @( @@ -398,13 +385,11 @@ RenewalCert = $Thumbprint if ($Credential) { - Import-Module -Name $PSScriptRoot\..\PDT\PDT.psm1 -Force - # Assemble the command and arguments to pass to the powershell process that # will request the certificate $certReqOutPath = [System.IO.Path]::ChangeExtension($workingPath,'.out') $command = "$PSHOME\PowerShell.exe" - $arguments = "-Command ""& $ENV:SystemRoot\system32\certreq.exe" + ` + $arguments = "-Command ""& $env:SystemRoot\system32\certreq.exe" + ` " @('-submit','-q','-config',$ca,'$reqPath','$cerPath')" + ` " | Set-Content -Path '$certReqOutPath'""" diff --git a/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.psm1 b/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.psm1 new file mode 100644 index 00000000..d657773d --- /dev/null +++ b/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.psm1 @@ -0,0 +1,480 @@ +#Requires -Version 4.0 + +$script:ResourceRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) + +# Import the xCertificate Resource Module (to import the common modules) +Import-Module -Name (Join-Path -Path $script:ResourceRootPath -ChildPath 'xCertificate.psd1') + +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'MSFT_xCertificateExport' ` + -ResourcePath (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) + +<# + .SYNOPSIS + Returns the current state of the exported certificate. + + .PARAMETER Path + The path to the file you that will contain the exported certificate. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.GettingCertificateExportMessage -f $Path) + ) -join '' ) + + $result = @{ + Path = $Path + IsExported = (Test-Path -Path $Path) + } + + return $result +} # end function Get-TargetResource + +<# + .SYNOPSIS + Exports the certificate. + + .PARAMETER Path + The path to the file you that will contain the exported certificate. + + .PARAMETER Thumbprint + The thumbprint of the certificate to export. + Certificate selector parameter. + + .PARAMETER FriendlyName + The friendly name of the certificate to export. + Certificate selector parameter. + + .PARAMETER Subject + The subject of the certificate to export. + Certificate selector parameter. + + .PARAMETER DNSName + The subject alternative name of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER Issuer + The issuer of the certiicate to export. + Certificate selector parameter. + + .PARAMETER KeyUsage + The key usage of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER EnhancedKeyUsage + The enhanced key usage of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER Store + The Windows Certificate Store Name to search for the certificate to export from. + Certificate selector parameter. + Defaults to 'My'. + + .PARAMETER AllowExpired + Allow an expired certificate to be exported. + Certificate selector parameter. + + .PARAMETER MatchSource + Causes an existing exported certificate to be compared with the certificate identified for + export and re-exported if it does not match. + + .PARAMETER Type + Specifies the type of certificate to export. + Defaults to 'Cert'. + + .PARAMETER ChainOption + Specifies the options for building a chain when exporting a PFX certificate. + Defaults to 'BuildChain'. + + .PARAMETER Password + Specifies the password used to protect an exported PFX file. + + .PARAMETER ProtectTo + Specifies an array of strings for the username or group name that can access the private + key of an exported PFX file without any password. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Thumbprint, + + [Parameter()] + [System.String] + $FriendlyName, + + [Parameter()] + [System.String] + $Subject, + + [Parameter()] + [System.String[]] + $DNSName, + + [Parameter()] + [System.String] + $Issuer, + + [Parameter()] + [System.String[]] + $KeyUsage, + + [Parameter()] + [System.String[]] + $EnhancedKeyUsage, + + [Parameter()] + [System.String] + $Store = 'My', + + [Parameter()] + [System.Boolean] + $AllowExpired, + + [Parameter()] + [System.Boolean] + $MatchSource, + + [Parameter()] + [ValidateSet("Cert","P7B","SST","PFX")] + [System.String] + $Type = 'Cert', + + [Parameter()] + [ValidateSet("BuildChain","EndEntityCertOnly")] + [System.String] + $ChainOption = 'BuildChain', + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.String[]] + $ProtectTo + ) + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.SettingCertificateExportMessage -f $Path) + ) -join '' ) + + $findCertificateParameters = @{} + $PSBoundParameters + $null = $findCertificateParameters.Remove('Path') + $null = $findCertificateParameters.Remove('MatchSource') + $null = $findCertificateParameters.Remove('Type') + $null = $findCertificateParameters.Remove('ChainOption') + $null = $findCertificateParameters.Remove('Password') + $null = $findCertificateParameters.Remove('ProtectTo') + $foundCertificates = @(Find-Certificate @findCertificateParameters) + + if ($foundCertificates.Count -eq 0) + { + # A certificate matching the specified certificate selector parameters could not be found + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateToExportNotFound -f $Path,$Type,$Store) + ) -join '' ) + } + else + { + $certificateToExport = $foundCertificates[0] + $certificateThumbprintToExport = $certificateToExport.Thumbprint + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateToExportFound -f $certificateThumbprintToExport,$Path) + ) -join '' ) + + # Export the certificate + $exportCertificateParameters = @{ + FilePath = $Path + Cert = $certificateToExport + Force = $true + } + + if ($Type -in @('Cert','P7B','SST')) + { + $exportCertificateParameters += @{ + Type = $Type + } + Export-Certificate @exportCertificateParameters + } + elseif ($Type -eq 'PFX') + { + $exportCertificateParameters += @{ + Password = $Password.Password + ChainOption = $ChainOption + } + + if ($PSBoundParameters.ContainsKey('ProtectTo')) + { + $exportCertificateParameters += @{ + ProtectTo = $ProtectTo + } + } # if + Export-PfxCertificate @exportCertificateParameters + } # if + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateExported -f $certificateThumbprintToExport,$Path,$Type) + ) -join '' ) + } # if +} # end function Set-TargetResource + +<# + .SYNOPSIS + Tests the state of the currently exported certificate. + + .PARAMETER Path + The path to the file you that will contain the exported certificate. + + .PARAMETER Thumbprint + The thumbprint of the certificate to export. + Certificate selector parameter. + + .PARAMETER FriendlyName + The friendly name of the certificate to export. + Certificate selector parameter. + + .PARAMETER Subject + The subject of the certificate to export. + Certificate selector parameter. + + .PARAMETER DNSName + The subject alternative name of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER Issuer + The issuer of the certiicate to export. + Certificate selector parameter. + + .PARAMETER KeyUsage + The key usage of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER EnhancedKeyUsage + The enhanced key usage of the certificate to export must contain these values. + Certificate selector parameter. + + .PARAMETER Store + The Windows Certificate Store Name to search for the certificate to export from. + Certificate selector parameter. + Defaults to 'My'. + + .PARAMETER AllowExpired + Allow an expired certificate to be exported. + Certificate selector parameter. + + .PARAMETER MatchSource + Causes an existing exported certificate to be compared with the certificate identified for + export and re-exported if it does not match. + + .PARAMETER Type + Specifies the type of certificate to export. + Defaults to 'Cert'. + + .PARAMETER ChainOption + Specifies the options for building a chain when exporting a PFX certificate. + Defaults to 'BuildChain'. + + .PARAMETER Password + Specifies the password used to protect an exported PFX file. + + .PARAMETER ProtectTo + Specifies an array of strings for the username or group name that can access the private + key of an exported PFX file without any password. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Thumbprint, + + [Parameter()] + [System.String] + $FriendlyName, + + [Parameter()] + [System.String] + $Subject, + + [Parameter()] + [System.String] + $Issuer, + + [Parameter()] + [System.String[]] + $DNSName, + + [Parameter()] + [System.String[]] + $KeyUsage, + + [Parameter()] + [System.String[]] + $EnhancedKeyUsage, + + [Parameter()] + [System.String] + $Store = 'My', + + [Parameter()] + [System.Boolean] + $AllowExpired, + + [Parameter()] + [System.Boolean] + $MatchSource, + + [Parameter()] + [ValidateSet("Cert","P7B","SST","PFX")] + [System.String] + $Type = 'Cert', + + [Parameter()] + [ValidateSet("BuildChain","EndEntityCertOnly")] + [System.String] + $ChainOption = 'BuildChain', + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.String[]] + $ProtectTo + ) + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.TestingCertificateExportMessage -f $Path) + ) -join '' ) + + $findCertificateParameters = @{} + $PSBoundParameters + $null = $findCertificateParameters.Remove('Path') + $null = $findCertificateParameters.Remove('MatchSource') + $null = $findCertificateParameters.Remove('Type') + $null = $findCertificateParameters.Remove('ChainOption') + $null = $findCertificateParameters.Remove('Password') + $null = $findCertificateParameters.Remove('ProtectTo') + $foundCertificates = @(Find-Certificate @findCertificateParameters) + + if ($foundCertificates.Count -eq 0) + { + # A certificate matching the specified certificate selector parameters could not be found + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateToExportNotFound -f $Path,$Type,$Store) + ) -join '' ) + + return $true + } + else + { + $certificateToExport = $foundCertificates[0] + $certificateThumbprintToExport = $certificateToExport.Thumbprint + + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateToExportFound -f $certificateThumbprintToExport,$Path) + ) -join '' ) + + if (Test-Path -Path $Path) + { + if ($MatchSource) + { + # The certificate has already been exported, but we need to make sure it matches + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateAlreadyExportedMatchSource -f $certificateThumbprintToExport,$Path) + ) -join '' ) + + # Need to now compare the existing exported cert content with the found cert + $exportedCertificate = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2Collection' + if ($Type -in @('Cert','P7B','SST')) + { + $exportedCertificate.Import($Path) + } + elseif ($Type -eq 'PFX') + { + $exportedCertificate.Import($Path,$Password,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) + } # if + + if ($certificateThumbprintToExport -notin $exportedCertificate.Thumbprint) + { + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateAlreadyExportedNotMatchSource -f $certificateThumbprintToExport,$Path) + ) -join '' ) + + return $false + } # if + } + else + { + # This certificate is already exported and we don't want to check it is + # the right certificate. + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateAlreadyExported -f $certificateThumbprintToExport,$Path) + ) -join '' ) + } # if + + return $true + } + else + { + # The found certificate has not been exported yet + Write-Verbose -Message ( + @( + "$($MyInvocation.MyCommand): ", + $($LocalizedData.CertificateNotExported -f $certificateThumbprintToExport,$Path) + ) -join '' ) + + return $false + } # if + } # if +} # end function Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.schema.mof b/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.schema.mof new file mode 100644 index 00000000..a8837057 --- /dev/null +++ b/DSCResources/MSFT_xCertificateExport/MSFT_xCertificateExport.schema.mof @@ -0,0 +1,20 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xCertificateExport")] +class MSFT_xCertificateExport : OMI_BaseResource +{ + [Key,Description("The path to the file you that will contain the exported certificate.")] string Path; + [Write,Description("The thumbprint of the certificate to export. Certificate selector parameter.")] string Thumbprint; + [Write,Description("The friendly name of the certificate to export. Certificate selector parameter.")] string FriendlyName; + [Write,Description("The subject of the certificate to export. Certificate selector parameter.")] string Subject; + [Write,Description("The subject alternative name of the certificate to export must contain these values. Certificate selector parameter.")] string DNSName[]; + [Write,Description("The issuer of the certiicate to export. Certificate selector parameter.")] string Issuer; + [Write,Description("The key usage of the certificate to export must contain these values. Certificate selector parameter.")] string KeyUsage[]; + [Write,Description("The enhanced key usage of the certificate to export must contain these values. Certificate selector parameter.")] string EnhancedKeyUsage[]; + [Write,Description("The Windows Certificate Store Name to search for the certificate to export from. Certificate selector parameter. Defaults to 'My'.")] string Store; + [Write,Description("Allow an expired certificate to be exported. Certificate selector parameter.")] boolean AllowExpired; + [Write,Description("Causes an existing exported certificate to be compared with the certificate identified for export and re-exported if it does not match.")] boolean MatchSource; + [Write,Description("Specifies the type of certificate to export."),ValueMap{"Cert", "P7B", "SST", "PFX"},Values{"Cert", "P7B", "SST", "PFX"}] string Type; + [Write,Description("Specifies the options for building a chain when exporting a PFX certificate."),ValueMap{"BuildChain","EndEntityCertOnly"},Values{"BuildChain","EndEntityCertOnly"}] string ChainOption; + [Write,Description("Specifies the password used to protect an exported PFX file."),EmbeddedInstance("MSFT_Credential")] String Password; + [Write,Description("Specifies an array of strings for the username or group name that can access the private key of an exported PFX file without any password.")] string ProtectTo[]; + [Read,Description("Returns true if the certificate file already exists and therefore has been exported.")] boolean IsExported; +}; diff --git a/DSCResources/MSFT_xCertificateExport/en-us/MSFT_xCertificateExport.strings.psd1 b/DSCResources/MSFT_xCertificateExport/en-us/MSFT_xCertificateExport.strings.psd1 new file mode 100644 index 00000000..1e57dd6c --- /dev/null +++ b/DSCResources/MSFT_xCertificateExport/en-us/MSFT_xCertificateExport.strings.psd1 @@ -0,0 +1,12 @@ +ConvertFrom-StringData @' + GettingCertificateExportMessage = Getting certificate export status to '{0}'. + SettingCertificateExportMessage = Setting certificate export status to '{0}'. + TestingCertificateExportMessage = Testing certificate export status to '{0}'. + CertificateToExportNotFound = Could not find certificate to Export to '{0}' as '{1}' in LocalMachine '{2}' store. + CertificateToExportFound = Certificate to export with thumbprint '{0}' found to export to '{1}'. + CertificateAlreadyExported = Certificate to export with thumbprint '{0}' has already been exported to '{1}'. MatchSource set to false so not checking content match. Will not export. + CertificateAlreadyExportedMatchSource = Certificate to Export with thumbprint '{0}' has already been exported to '{1}'. MatchSource set to true so checking content match. + CertificateAlreadyExportedNotMatchSource = Certificate to Export with thumbprint '{0}' has already been exported to '{1}'. but exported certificate does not contain expected thumbprint. Will export. + CertificateNotExported = Certificate to export with thumbprint '{0}' has not yet been exported to '{1}'. Will export. + CertificateExported = Certificate to export as '{2}' with thumbprint '{0}' was exported to '{1}'. +'@ diff --git a/DSCResources/MSFT_xCertificateImport/MSFT_xCertificateImport.psm1 b/DSCResources/MSFT_xCertificateImport/MSFT_xCertificateImport.psm1 index 4eb1dbcd..b5b3422b 100644 --- a/DSCResources/MSFT_xCertificateImport/MSFT_xCertificateImport.psm1 +++ b/DSCResources/MSFT_xCertificateImport/MSFT_xCertificateImport.psm1 @@ -1,27 +1,14 @@ #Requires -Version 4.0 -#region localizeddata -if (Test-Path "${PSScriptRoot}\${PSUICulture}") -{ - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xCertificateImport.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\${PSUICulture}" -} -else -{ - #fallback to en-US - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xCertificateImport.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\en-US" -} -#endregion - -# Import the common certificate functions -Import-Module -Name ( Join-Path ` - -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'CertificateCommon\CertificateCommon.psm1' ) +$script:ResourceRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) + +# Import the xCertificate Resource Module (to import the common modules) +Import-Module -Name (Join-Path -Path $script:ResourceRootPath -ChildPath 'xCertificate.psd1') + +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'MSFT_xCertificateImport' ` + -ResourcePath (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) <# .SYNOPSIS diff --git a/DSCResources/MSFT_xPfxImport/MSFT_xPfxImport.psm1 b/DSCResources/MSFT_xPfxImport/MSFT_xPfxImport.psm1 index 368f428b..f8a775d1 100644 --- a/DSCResources/MSFT_xPfxImport/MSFT_xPfxImport.psm1 +++ b/DSCResources/MSFT_xPfxImport/MSFT_xPfxImport.psm1 @@ -1,27 +1,14 @@ #Requires -Version 4.0 -#region localizeddata -if (Test-Path "${PSScriptRoot}\${PSUICulture}") -{ - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xPfxImport.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\${PSUICulture}" -} -else -{ - #fallback to en-US - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename MSFT_xPfxImport.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\en-US" -} -#endregion - -# Import the common certificate functions -Import-Module -Name ( Join-Path ` - -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'CertificateCommon\CertificateCommon.psm1' ) +$script:ResourceRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) + +# Import the xCertificate Resource Module (to import the common modules) +Import-Module -Name (Join-Path -Path $script:ResourceRootPath -ChildPath 'xCertificate.psd1') + +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'MSFT_xPfxImport' ` + -ResourcePath (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) <# .SYNOPSIS diff --git a/Examples/Resources/xCertReq/1-RequestAltSSLCert.ps1 b/Examples/Resources/xCertReq/1-RequestAltSSLCert.ps1 new file mode 100644 index 00000000..1900f957 --- /dev/null +++ b/Examples/Resources/xCertReq/1-RequestAltSSLCert.ps1 @@ -0,0 +1,44 @@ +<# + .EXAMPLE + Request and Accept a certificate from an Active Directory Root Certificate Authority. This certificate + is issued using an subject alternate name with multiple DNS addresses. + + This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. + Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. + To learn how to securely store credentials through the use of certificates, + please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx +#> +configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [PSCredential] + $Credential + ) + + Import-DscResource -ModuleName xCertificate + Node 'localhost' + { + xCertReq SSLCert + { + CARootName = 'test-dc01-ca' + CAServerFQDN = 'dc01.test.pha' + Subject = 'contoso.com' + KeyLength = '1024' + Exportable = $true + ProviderName = '"Microsoft RSA SChannel Cryptographic Provider"' + OID = '1.3.6.1.5.5.7.3.1' + KeyUsage = '0xa0' + CertificateTemplate = 'WebServer' + SubjectAltName = 'dns=fabrikam.com&dns=contoso.com' + AutoRenew = $true + Credential = $Credential + } + } +} diff --git a/Examples/Resources/xCertReq/2-RequestSSLCert.ps1 b/Examples/Resources/xCertReq/2-RequestSSLCert.ps1 new file mode 100644 index 00000000..711b8546 --- /dev/null +++ b/Examples/Resources/xCertReq/2-RequestSSLCert.ps1 @@ -0,0 +1,42 @@ +<# + .EXAMPLE + Request and Accept a certificate from an Active Directory Root Certificate Authority. + + This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. + Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. + To learn how to securely store credentials through the use of certificates, + please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx +#> +configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [PSCredential] + $Credential + ) + + Import-DscResource -ModuleName xCertificate + Node 'localhost' + { + xCertReq SSLCert + { + CARootName = 'test-dc01-ca' + CAServerFQDN = 'dc01.test.pha' + Subject = 'foodomain.test.net' + KeyLength = '1024' + Exportable = $true + ProviderName = '"Microsoft RSA SChannel Cryptographic Provider"' + OID = '1.3.6.1.5.5.7.3.1' + KeyUsage = '0xa0' + CertificateTemplate = 'WebServer' + AutoRenew = $true + Credential = $Credential + } + } +} diff --git a/Examples/Resources/xCertificateExport/1-CertByFriendlyName.ps1 b/Examples/Resources/xCertificateExport/1-CertByFriendlyName.ps1 new file mode 100644 index 00000000..e0ef1041 --- /dev/null +++ b/Examples/Resources/xCertificateExport/1-CertByFriendlyName.ps1 @@ -0,0 +1,25 @@ +<# + .EXAMPLE + Exports a certificate as a CERT using the friendly name to identify it. +#> +Configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost' + ) + + Import-DscResource -ModuleName xCertificate + + Node $AllNodes.NodeName + { + xCertificateExport SSLCert + { + Type = 'CERT' + FriendlyName = 'Web Site SSL Certificate for www.contoso.com' + Path = 'c:\sslcert.cer' + } + } +} diff --git a/Examples/Resources/xCertificateExport/2-PfxByFriendlyName.ps1 b/Examples/Resources/xCertificateExport/2-PfxByFriendlyName.ps1 new file mode 100644 index 00000000..bdfcbe83 --- /dev/null +++ b/Examples/Resources/xCertificateExport/2-PfxByFriendlyName.ps1 @@ -0,0 +1,31 @@ +<# + .EXAMPLE + Exports a certificate as a PFX using the friendly name to identify it. +#> +Configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [PSCredential] + $Credential + ) + + Import-DscResource -ModuleName xCertificate + + Node $AllNodes.NodeName + { + xCertificateExport SSLCert + { + Type = 'PFX' + FriendlyName = 'Web Site SSL Certificate for www.contoso.com' + Path = 'c:\sslcert.cer' + Password = $Credential + } + } +} diff --git a/Examples/Sample_xCertificateImport_MinimalUsage.ps1 b/Examples/Resources/xCertificateImport/1-MinimalUsage.ps1 similarity index 52% rename from Examples/Sample_xCertificateImport_MinimalUsage.ps1 rename to Examples/Resources/xCertificateImport/1-MinimalUsage.ps1 index 9a8864d2..38962922 100644 --- a/Examples/Sample_xCertificateImport_MinimalUsage.ps1 +++ b/Examples/Resources/xCertificateImport/1-MinimalUsage.ps1 @@ -1,6 +1,16 @@ -# Import public key certificate into Trusted Root store. -Configuration Sample_xCertificateImport_MinimalUsage +<# + .EXAMPLE + Import public key certificate into Trusted Root store. +#> +Configuration Example { + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost' + ) + Import-DscResource -ModuleName xCertificate Node $AllNodes.NodeName @@ -14,9 +24,3 @@ Configuration Sample_xCertificateImport_MinimalUsage } } } -Sample_xCertificateImport_MinimalUsage ` - -OutputPath 'c:\Sample_xCertificateImport_MinimalUsage' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xCertificateImport_MinimalUsage' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My diff --git a/Examples/Sample_xPfxImport_IIS_WebSite.ps1 b/Examples/Resources/xPfxImport/1-IIS_WebSite.ps1 similarity index 75% rename from Examples/Sample_xPfxImport_IIS_WebSite.ps1 rename to Examples/Resources/xPfxImport/1-IIS_WebSite.ps1 index faac629a..71793800 100644 --- a/Examples/Sample_xPfxImport_IIS_WebSite.ps1 +++ b/Examples/Resources/xPfxImport/1-IIS_WebSite.ps1 @@ -1,10 +1,19 @@ -# Import a PFX into the WebHosting store and bind it to an IIS Web Site. -Configuration Sample_xPfxImport_IIS_WebSite +<# + .EXAMPLE + Import a PFX into the WebHosting store and bind it to an IIS Web Site. +#> +Configuration Example { param ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] [PSCredential] - $PfxPassword = (Get-Credential -Message 'Enter PFX extraction password.' -UserName 'Ignore') + $Credential ) Import-DscResource -ModuleName xCertificate @@ -23,7 +32,7 @@ Configuration Sample_xPfxImport_IIS_WebSite Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' Path = '\\Server\Share\Certificates\CompanyCert.pfx' Store = 'WebHosting' - Credential = $PfxPassword + Credential = $Credential DependsOn = '[WindowsFeature]IIS' } @@ -46,6 +55,3 @@ Configuration Sample_xPfxImport_IIS_WebSite } } } -Sample_xPfxImport_IIS_WebSite ` - -OutputPath 'c:\Sample_xPfxImport_IIS_WebSite' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xPfxImport_IIS_WebSite' diff --git a/Examples/Resources/xPfxImport/2-MinimalUsage.ps1 b/Examples/Resources/xPfxImport/2-MinimalUsage.ps1 new file mode 100644 index 00000000..016527eb --- /dev/null +++ b/Examples/Resources/xPfxImport/2-MinimalUsage.ps1 @@ -0,0 +1,30 @@ +<# + .EXAMPLE + Import a PFX into the My store. +#> +Configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [PSCredential] + $Credential + ) + + Import-DscResource -ModuleName xCertificate + + Node $AllNodes.NodeName + { + xPfxImport CompanyCert + { + Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' + Path = '\\Server\Share\Certificates\CompanyCert.pfx' + Credential = $Credential + } + } +} diff --git a/Examples/Sample_xCertReq_RequestAltSSLCert.ps1 b/Examples/Sample_xCertReq_RequestAltSSLCert.ps1 deleted file mode 100644 index 3e9edb05..00000000 --- a/Examples/Sample_xCertReq_RequestAltSSLCert.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -<# - Request and Accept a certificate from an Active Directory Root Certificate Authority. This certificate - is issued using an subject alternate name with multiple DNS addresses. - - This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. - Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. - To learn how to securely store credentials through the use of certificates, - please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx -#> -configuration Sample_xCertReq_RequestAltSSL -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [PSCredential] $Credential - ) - - Import-DscResource -ModuleName xCertificate - Node 'localhost' - { - xCertReq SSLCert - { - CARootName = 'test-dc01-ca' - CAServerFQDN = 'dc01.test.pha' - Subject = 'contoso.com' - KeyLength = '1024' - Exportable = $true - ProviderName = '"Microsoft RSA SChannel Cryptographic Provider"' - OID = '1.3.6.1.5.5.7.3.1' - KeyUsage = '0xa0' - CertificateTemplate = 'WebServer' - SubjectAltName = 'dns=fabrikam.com&dns=contoso.com' - AutoRenew = $true - Credential = $Credential - } - } -} -$configData = @{ - AllNodes = @( - @{ - NodeName = 'localhost'; - PSDscAllowPlainTextPassword = $true - } - ) - } -Sample_xCertReq_RequestSSL ` - -ConfigurationData $configData ` - -Credential (Get-Credential) ` - -OutputPath 'c:\Sample_xCertReq_RequestAltSSL' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xCertReq_RequestAltSSL' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My diff --git a/Examples/Sample_xCertReq_RequestSSLCert.ps1 b/Examples/Sample_xCertReq_RequestSSLCert.ps1 deleted file mode 100644 index 84c7daa6..00000000 --- a/Examples/Sample_xCertReq_RequestSSLCert.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -<# - Request and Accept a certificate from an Active Directory Root Certificate Authority. - - This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. - Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. - To learn how to securely store credentials through the use of certificates, - please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx -#> -configuration Sample_xCertReq_RequestSSL -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [PSCredential] $Credential - ) - - Import-DscResource -ModuleName xCertificate - Node 'localhost' - { - xCertReq SSLCert - { - CARootName = 'test-dc01-ca' - CAServerFQDN = 'dc01.test.pha' - Subject = 'foodomain.test.net' - KeyLength = '1024' - Exportable = $true - ProviderName = '"Microsoft RSA SChannel Cryptographic Provider"' - OID = '1.3.6.1.5.5.7.3.1' - KeyUsage = '0xa0' - CertificateTemplate = 'WebServer' - AutoRenew = $true - Credential = $Credential - } - } -} -$configData = @{ - AllNodes = @( - @{ - NodeName = 'localhost'; - PSDscAllowPlainTextPassword = $true - } - ) - } -Sample_xCertReq_RequestSSL ` - -ConfigurationData $configData ` - -Credential (Get-Credential) ` - -OutputPath 'c:\Sample_xCertReq_RequestSSL' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xCertReq_RequestSSL' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My diff --git a/Examples/Sample_xPfxImport_MinimalUsage.ps1 b/Examples/Sample_xPfxImport_MinimalUsage.ps1 deleted file mode 100644 index 61bf9ecf..00000000 --- a/Examples/Sample_xPfxImport_MinimalUsage.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -# Import a PFX into the My store. -Configuration Sample_xPfxImport_MinimalUsage -{ - param - ( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [String] $PfxThumbprint, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [String] $PfxPath, - - [PSCredential] - $PfxPassword = (Get-Credential -Message 'Enter PFX extraction password.' -UserName 'Ignore') - ) - - Import-DscResource -ModuleName xCertificate - - Node $AllNodes.NodeName - { - xPfxImport CompanyCert - { - Thumbprint = $PfxThumbprint - Path = $PfxPath - Credential = $PfxPassword - } - } -} - -$Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' - -Sample_xPfxImport_MinimalUsage ` - -PfxThumbprint $Thumbprint ` - -PfxPath '\\Server\Share\Certificates\CompanyCert.pfx' ` - -OutputPath 'c:\Sample_xPfxImport_MinimalUsage' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xPfxImport_MinimalUsage' - -# Validate results - a Certificate with a thumbprint matching $Thumbprint should be returned -Get-ChildItem -Path Cert:\LocalMachine\My | - Where-Object -FilterScript { $_.Thumbprint -eq $Thumbprint } diff --git a/DSCResources/CertificateCommon/CertificateCommon.psm1 b/Modules/CertificateDsc.Common/CertificateDSc.Common.psm1 similarity index 51% rename from DSCResources/CertificateCommon/CertificateCommon.psm1 rename to Modules/CertificateDsc.Common/CertificateDSc.Common.psm1 index d31bfda8..aa005ca4 100644 --- a/DSCResources/CertificateCommon/CertificateCommon.psm1 +++ b/Modules/CertificateDsc.Common/CertificateDSc.Common.psm1 @@ -1,20 +1,12 @@ -#region localizeddata -if (Test-Path "${PSScriptRoot}\${PSUICulture}") -{ - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename CertificateCommon.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\${PSUICulture}" -} -else -{ - #fallback to en-US - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename CertificateCommon.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\en-US" -} -#endregion +# Import the Networking Resource Helper Module +Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath (Join-Path -Path 'CertificateDsc.ResourceHelper' ` + -ChildPath 'CertificateDsc.ResourceHelper.psm1')) + +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'CertificateDsc.Common' ` + -ResourcePath $PSScriptRoot <# .SYNOPSIS @@ -45,7 +37,8 @@ function Test-CertificatePath [CmdletBinding()] param ( - [Parameter(Mandatory,ValueFromPipeline)] + [Parameter(Mandatory = $true, + ValueFromPipeline)] [String[]] $Path, @@ -107,7 +100,8 @@ function Test-Thumbprint [CmdletBinding()] param ( - [Parameter(Mandatory,ValueFromPipeline)] + [Parameter(Mandatory = $true, + ValueFromPipeline)] [ValidateNotNullOrEmpty()] [String[]] $Thumbprint, @@ -174,37 +168,152 @@ function Test-Thumbprint <# .SYNOPSIS - Throws an InvalidOperation custom exception. + Locates one or more certificates using the passed certificate selector parameters. - .PARAMETER ErrorId - The error Id of the exception. + If more than one certificate is found matching the selector criteria, they will be + returned in order of descending expiration date. + + .PARAMETER Thumbprint + The thumbprint of the certificate to find. + + .PARAMETER FriendlyName + The friendly name of the certificate to find. + + .PARAMETER Subject + The subject of the certificate to find. + + .PARAMETER DNSName + The subject alternative name of the certificate to export must contain these values. + + .PARAMETER Issuer + The issuer of the certiicate to find. + + .PARAMETER KeyUsage + The key usage of the certificate to find must contain these values. + + .PARAMETER EnhancedKeyUsage + The enhanced key usage of the certificate to find must contain these values. + + .PARAMETER Store + The Windows Certificate Store Name to search for the certificate in. + Defaults to 'My'. + + .PARAMETER AllowExpired + Allows expired certificates to be returned. - .PARAMETER ErrorMessage - The error message text to set in the exception. #> -function New-InvalidOperationError +function Find-Certificate { [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2[]])] param ( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [System.String] - $ErrorId, + [Parameter()] + [String] + $Thumbprint, - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [System.String] - $ErrorMessage + [Parameter()] + [String] + $FriendlyName, + + [Parameter()] + [String] + $Subject, + + [Parameter()] + [String[]] + $DNSName, + + [Parameter()] + [String] + $Issuer, + + [Parameter()] + [String[]] + $KeyUsage, + + [Parameter()] + [String[]] + $EnhancedKeyUsage, + + [Parameter()] + [String] + $Store = 'My', + + [Parameter()] + [Boolean] + $AllowExpired = $false ) - $exception = New-Object -TypeName System.InvalidOperationException ` - -ArgumentList $ErrorMessage - $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation - $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` - -ArgumentList $exception, $ErrorId, $errorCategory, $null - throw $errorRecord -} # end function New-InvalidOperationError + $certPath = Join-Path -Path 'Cert:\LocalMachine' -ChildPath $Store + + if (-not (Test-Path -Path $certPath)) + { + # The Certificte Path is not valid + New-InvalidArgumentError ` + -ErrorId 'CannotFindCertificatePath' ` + -ErrorMessage ($LocalizedData.CertificatePathError -f $certPath) + } # if + + # Assemble the filter to use to select the certificate + $certFilters = @() + if ($PSBoundParameters.ContainsKey('Thumbprint')) + { + $certFilters += @('($_.Thumbprint -eq $Thumbprint)') + } # if + + if ($PSBoundParameters.ContainsKey('FriendlyName')) + { + $certFilters += @('($_.FriendlyName -eq $FriendlyName)') + } # if + + if ($PSBoundParameters.ContainsKey('Subject')) + { + $certFilters += @('($_.Subject -eq $Subject)') + } # if + + if ($PSBoundParameters.ContainsKey('Issuer')) + { + $certFilters += @('($_.Issuer -eq $Issuer)') + } # if + + if (-not $AllowExpired) + { + $certFilters += @('(((Get-Date) -le $_.NotAfter) -and ((Get-Date) -ge $_.NotBefore))') + } # if + + if ($PSBoundParameters.ContainsKey('DNSName')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject $_.DNSNameList.Unicode -DifferenceObject $DNSName | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('KeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.Extensions.KeyUsages -split ", ") -DifferenceObject $KeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('EnhancedKeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.EnhancedKeyUsageList.FriendlyName) -DifferenceObject $EnhancedKeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + # Join all the filters together + $certFilterScript = '(' + ($certFilters -join ' -and ') + ')' + + Write-Verbose -Message ($LocalizedData.SearchingForCertificateUsingFilters ` + -f $store,$certFilterScript) + + $certs = Get-ChildItem -Path $certPath | + Where-Object -FilterScript ([ScriptBlock]::Create($certFilterScript)) + + # Sort the certificates + if ($certs.count -gt 1) + { + $certs = $certs | Sort-Object -Descending -Property 'NotAfter' + } # if + + return $certs +} # end function Find-Certificate <# .SYNOPSIS diff --git a/Modules/CertificateDsc.Common/en-us/CertificateDsc.Common.strings.psd1 b/Modules/CertificateDsc.Common/en-us/CertificateDsc.Common.strings.psd1 new file mode 100644 index 00000000..77cabacc --- /dev/null +++ b/Modules/CertificateDsc.Common/en-us/CertificateDsc.Common.strings.psd1 @@ -0,0 +1,8 @@ +ConvertFrom-StringData @' + FileNotFoundError = File '{0}' not found. + InvalidHashError = '{0}' is not a valid hash. + CertificatePathError = Certificate Path '{0}' is not valid. + SearchingForCertificateUsingFilters = Looking for certificate in Store '{0}' using filter '{1}'. + + ValidHashMessage = '{0}' is a valid {1} hash. +'@ diff --git a/DSCResources/PDT/PDT.psm1 b/Modules/CertificateDsc.PDT/CertificateDsc.PDT.psm1 similarity index 92% rename from DSCResources/PDT/PDT.psm1 rename to Modules/CertificateDsc.PDT/CertificateDsc.PDT.psm1 index d6bd2e74..2c146193 100644 --- a/DSCResources/PDT/PDT.psm1 +++ b/Modules/CertificateDsc.PDT/CertificateDsc.PDT.psm1 @@ -1,25 +1,12 @@ -#region localizeddata -if (Test-Path "${PSScriptRoot}\${PSUICulture}") -{ - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename PDT.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\${PSUICulture}" -} -else -{ - #fallback to en-US - Import-LocalizedData ` - -BindingVariable LocalizedData ` - -Filename PDT.strings.psd1 ` - -BaseDirectory "${PSScriptRoot}\en-US" -} -#endregion +# Import the Networking Resource Helper Module +Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath (Join-Path -Path 'CertificateDsc.ResourceHelper' ` + -ChildPath 'CertificateDsc.ResourceHelper.psm1')) -# Import the common certificate functions -Import-Module -Name ( Join-Path ` - -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'CertificateCommon\CertificateCommon.psm1' ) +# Import Localization Strings +$localizedData = Get-LocalizedData ` + -ResourceName 'CertificateDsc.PDT' ` + -ResourcePath $PSScriptRoot <# .SYNOPSIS @@ -44,9 +31,12 @@ function Get-Arguments $FunctionBoundParameters, [parameter(Mandatory = $true)] - [string[]] $ArgumentNames, + [string[]] + $ArgumentNames, - [string[]] $NewArgumentNames + [Parameter()] + [string[]] + $NewArgumentNames ) $returnValue=@{} @@ -389,11 +379,16 @@ function Get-Win32Process ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] $Path, + [String] + $Path, - [String] $Arguments, + [Parameter()] + [String] + $Arguments, - [PSCredential] $Credential + [Parameter()] + [PSCredential] + $Credential ) $fileName = [io.path]::GetFileNameWithoutExtension($Path) @@ -474,7 +469,9 @@ function Get-Win32ProcessArgumentsFromCommandLine { param ( - [String] $CommandLine + [Parameter()] + [String] + $CommandLine ) if ($commandLine -eq $null) @@ -521,11 +518,16 @@ function Start-Win32Process ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] $Path, + [String] + $Path, - [String] $Arguments, + [Parameter()] + [String] + $Arguments, - [PSCredential] $Credential + [Parameter()] + [PSCredential] + $Credential ) $getArguments = Get-Arguments $PSBoundParameters ("Path","Arguments","Credential") @@ -611,13 +613,20 @@ function Wait-Win32ProcessStart ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] $Path, + [String] + $Path, - [String] $Arguments, + [Parameter()] + [String] + $Arguments, - [PSCredential] $Credential, + [Parameter()] + [PSCredential] + $Credential, - [Int] $Timeout = 5000 + [Parameter()] + [Int] + $Timeout = 5000 ) $start = [DateTime]::Now @@ -655,13 +664,20 @@ function Wait-Win32ProcessStop ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] $Path, + [String] + $Path, - [String] $Arguments, + [Parameter()] + [String] + $Arguments, - [PSCredential] $Credential, + [Parameter()] + [PSCredential] + $Credential, - [Int] $Timeout = 30000 + [Parameter()] + [Int] + $Timeout = 30000 ) $start = [DateTime]::Now @@ -698,11 +714,16 @@ function Wait-Win32ProcessEnd ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] $Path, + [String] + $Path, - [String] $Arguments, + [Parameter()] + [String] + $Arguments, - [PSCredential] $Credential + [Parameter()] + [PSCredential] + $Credential ) $getArguments = Get-Arguments $PSBoundParameters ("Path","Arguments","Credential") diff --git a/DSCResources/PDT/en-us/PDT.strings.psd1 b/Modules/CertificateDsc.PDT/en-us/CertificateDsc.PDT.strings.psd1 similarity index 100% rename from DSCResources/PDT/en-us/PDT.strings.psd1 rename to Modules/CertificateDsc.PDT/en-us/CertificateDsc.PDT.strings.psd1 diff --git a/Modules/CertificateDsc.ResourceHelper/CertificateDsc.ResourceHelper.psm1 b/Modules/CertificateDsc.ResourceHelper/CertificateDsc.ResourceHelper.psm1 new file mode 100644 index 00000000..5956e436 --- /dev/null +++ b/Modules/CertificateDsc.ResourceHelper/CertificateDsc.ResourceHelper.psm1 @@ -0,0 +1,174 @@ +<# + .SYNOPSIS + Tests if the current machine is a Nano server. +#> +function Test-IsNanoServer +{ + if (Test-Command -Name Get-ComputerInfo) + { + $computerInfo = Get-ComputerInfo + + if ("Server" -eq $computerInfo.OsProductType ` + -and "NanoServer" -eq $computerInfo.OsServerLevel) + { + return $true + } + } + + return $false +} + +<# + .SYNOPSIS + Tests if the the specified command is found. +#> +function Test-Command +{ + param + ( + [Parameter()] + [String] + $Name + ) + + return ($null -ne (Get-Command -Name $Name -ErrorAction Continue 2> $null)) +} + +<# + .SYNOPSIS + Creates and throws an invalid argument exception + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown +#> +function New-InvalidArgumentException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' -ArgumentList @( $Message, + $ArgumentName ) + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $argumentException, $ArgumentName, 'InvalidArgument', $null ) + } + $errorRecord = New-Object @newObjectParams + + throw $errorRecord +} + +<# + .SYNOPSIS + Creates and throws an invalid operation exception + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error +#> +function New-InvalidOperationException +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $Message) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' + } + elseif ($null -eq $ErrorRecord) + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message ) + } + else + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message, + $ErrorRecord.Exception ) + } + + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $invalidOperationException.ToString(), 'MachineStateIncorrect', + 'InvalidOperation', $null ) + } + $errorRecordToThrow = New-Object @newObjectParams + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Retrieves the localized string data based on the machine's culture. + Falls back to en-US strings if the machine's culture is not supported. + + .PARAMETER ResourceName + The name of the resource as it appears before '.strings.psd1' of the localized string file. + + For example: + For WindowsOptionalFeature: MSFT_xWindowsOptionalFeature + For Service: MSFT_xServiceResource + For Registry: MSFT_xRegistryResource + + .PARAMETER ResourcePath + The path the resource file is located in. +#> +function Get-LocalizedData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ResourceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ResourcePath + ) + + $localizedStringFileLocation = Join-Path -Path $ResourcePath -ChildPath $PSUICulture + + if (-not (Test-Path -Path $localizedStringFileLocation)) + { + # Fallback to en-US + $localizedStringFileLocation = Join-Path -Path $ResourcePath -ChildPath 'en-US' + } + + Import-LocalizedData ` + -BindingVariable 'localizedData' ` + -FileName "$ResourceName.strings.psd1" ` + -BaseDirectory $localizedStringFileLocation + + return $localizedData +} + +Export-ModuleMember -Function @( 'Test-IsNanoServer', 'New-InvalidArgumentException', + 'New-InvalidOperationException', 'Get-LocalizedData' ) diff --git a/ReadMe.md b/ReadMe.md index 50c31fe9..e2c25bff 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -9,6 +9,7 @@ The **xCertificate** module contains the following resources: - **xCertReq** - **xPfxImport** - **xCertificateImport** +- **xCertificateExport** This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. @@ -52,10 +53,39 @@ Please check out common DSC Resources [contributing guidelines](https://github.c - **`[String]` Store** (_Key_): The Windows Certificate Store Name to import the certificate to. - **`[String]` Ensure** (_Write_): Specifies whether the certificate should be present or absent. { *Present* | Absent }. +### xCertificateExport + +- **`[String]` Path** (_Key_): The path to the file you that will contain the exported certificate. +- **`[String]` Thumbprint** (_Write_): The thumbprint of the certificate to export. Certificate selector parameter. +- **`[String]` FriendlyName** (_Write_): The friendly name of the certificate to export. Certificate selector parameter. +- **`[String]` Subject** (_Write_): The subject of the certificate to export. Certificate selector parameter. +- **`[String]` DNSName** (_Write_): The subject alternative name of the certificate to export must contain these values. Certificate selector parameter. +- **`[String]` Issuer** (_Write_): The issuer of the certiicate to export. Certificate selector parameter. +- **`[String[]]` KeyUsage** (_Write_): The key usage of the certificate to export must contain these values. Certificate selector parameter. +- **`[String[]]` EnhancedKeyUsage** (_Write_): The enhanced key usage of the certificate to export must contain these values. Certificate selector parameter. +- **`[String]` Store** (_Write_): The Windows Certificate Store Name to search for the certificate to export from. Certificate selector parameter. Defaults to 'My'. +- **`[Boolean]` AllowExpired** (_Write_): Allow an expired certificate to be exported. Certificate selector parameter. +- **`[Boolean]` MatchSource** (_Write_): Causes an existing exported certificate to be compared with the certificate identified for export and re-exported if it does not match. +- **`[String]` Type** (_Write_): Specifies the type of certificate to export. { *Cert* | P7B | SST | PFX } +- **`[String]` ChainOption** (_Write_): Specifies the options for building a chain when exporting a PFX certificate. { *BuildChain* | EndEntityCertOnly } +- **`[PSCredential]` Password** (_Write_): Specifies the password used to protect an exported PFX file. +- **`[String[]]` ProtectTo** (_Write_): Specifies an array of strings for the username or group name that can access the private key of an exported PFX file without any password. +- **`[Boolean]` IsExported** (_Read_): Returns true if the certificate file already exists and therefore has been exported. + ## Versions ### Unreleased +- Converted AppVeyor build process to use AppVeyor.psm1. +- Correct Param block to meet guidelines. +- Moved shared modules into modules folder. +- xCertificateExport: + - Added new resource. +- Cleanup xCertificate.psd1 to remove unneccessary properties. +- Converted AppVeyor.yml to use DSCResource.tests shared code. +- Opted-In to markdown rule validation. +- Examples modified to meet standards for auto documentation generation. + ### 2.3.0.0 - xCertReq: @@ -140,20 +170,30 @@ Please check out common DSC Resources [contributing guidelines](https://github.c ## Examples -### xCertReq +### xCertReq Examples #### Request an SSL Certificate Request and Accept a certificate from an Active Directory Root Certificate Authority. +This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. +Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. +To learn how to securely store credentials through the use of certificates, +please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx + ```powershell -configuration xCertReq_RequestSSL +configuration Example { param ( - [Parameter(Mandatory=$true)] + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] - [PsCredential] $Credential + [PSCredential] + $Credential ) Import-DscResource -ModuleName xCertificate @@ -175,37 +215,31 @@ configuration xCertReq_RequestSSL } } } -$configData = @{ - AllNodes = @( - @{ - NodeName = 'localhost'; - PSDscAllowPlainTextPassword = $true - } - ) - } -xCertReq_RequestSSL ` - -ConfigurationData $configData ` - -Credential (Get-Credential) ` - -OutputPath 'c:\xCertReq_RequestSSL' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\xCertReq_RequestSSL' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My ``` #### Request an SSL Certificate with alternative DNS names -Request and Accept a certificate from an Active Directory Root Certificate Authority. -This certificate is issued using an subject alternate name with multiple DNS addresses. +Request and Accept a certificate from an Active Directory Root Certificate Authority. This certificate +is issued using an subject alternate name with multiple DNS addresses. + +This example is allowing storage of credentials in plain text by setting PSDscAllowPlainTextPassword to $true. +Storing passwords in plain text is not a good practice and is presented only for simplicity and demonstration purposes. +To learn how to securely store credentials through the use of certificates, +please refer to the following TechNet topic: https://technet.microsoft.com/en-us/library/dn781430.aspx ```powershell -configuration Sample_xCertReq_RequestAltSSL +configuration Example { param ( - [Parameter(Mandatory=$true)] + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] - [PSCredential] $Credential + [PSCredential] + $Credential ) Import-DscResource -ModuleName xCertificate @@ -228,37 +262,27 @@ configuration Sample_xCertReq_RequestAltSSL } } } -$configData = @{ - AllNodes = @( - @{ - NodeName = 'localhost'; - PSDscAllowPlainTextPassword = $true - } - ) - } -Sample_xCertReq_RequestSSL ` - -ConfigurationData $configData ` - -Credential (Get-Credential) ` - -OutputPath 'c:\Sample_xCertReq_RequestAltSSL' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xCertReq_RequestAltSSL' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My ``` -### xPfxImport +### xPfxImport Examples #### Simple Usage Import a PFX into the My store. ```powershell -Configuration Sample_xPfxImport_MinimalUsage +Configuration Example { param ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] [PSCredential] - $PfxPassword = (Get-Credential -Message 'Enter PFX extraction password.' -UserName 'Ignore') + $Credential ) Import-DscResource -ModuleName xCertificate @@ -269,16 +293,10 @@ Configuration Sample_xPfxImport_MinimalUsage { Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' Path = '\\Server\Share\Certificates\CompanyCert.pfx' - Credential = $PfxPassword + Credential = $Credential } } } -Sample_xPfxImport_MinimalUsage ` - -OutputPath 'c:\Sample_xPfxImport_MinimalUsage' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xPfxImport_MinimalUsage' - -# Validate results -Get-ChildItem Cert:\LocalMachine\My ``` #### Used with xWebAdministration Resources @@ -286,12 +304,18 @@ Get-ChildItem Cert:\LocalMachine\My Import a PFX into the WebHosting store and bind it to an IIS Web Site. ```powershell -Configuration Sample_xPfxImport_IIS_WebSite +Configuration Example { param ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] [PSCredential] - $PfxPassword = (Get-Credential -Message 'Enter PFX extraction password.' -UserName 'Ignore') + $Credential ) Import-DscResource -ModuleName xCertificate @@ -310,7 +334,7 @@ Configuration Sample_xPfxImport_IIS_WebSite Thumbprint = 'c81b94933420221a7ac004a90242d8b1d3e5070d' Path = '\\Server\Share\Certificates\CompanyCert.pfx' Store = 'WebHosting' - Credential = $PfxPassword + Credential = $Credential DependsOn = '[WindowsFeature]IIS' } @@ -333,18 +357,22 @@ Configuration Sample_xPfxImport_IIS_WebSite } } } -Sample_xPfxImport_IIS_WebSite ` - -OutputPath 'c:\Sample_xPfxImport_IIS_WebSite' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xPfxImport_IIS_WebSite' ``` -### xCertificateImport +### xCertificateImport Examples Import public key certificate into Trusted Root store. ```powershell -Configuration Sample_xCertificateImport_MinimalUsage +Configuration Example { + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost' + ) + Import-DscResource -ModuleName xCertificate Node $AllNodes.NodeName @@ -358,10 +386,64 @@ Configuration Sample_xCertificateImport_MinimalUsage } } } -Sample_xCertificateImport_MinimalUsage ` - -OutputPath 'c:\Sample_xCertificateImport_MinimalUsage' -Start-DscConfiguration -Wait -Force -Verbose -Path 'c:\Sample_xCertificateImport_MinimalUsage' +``` + +### xCertificateExport Examples + +Exports a certificate as a CERT using the friendly name to identify it. + +```powershell +Configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost' + ) + + Import-DscResource -ModuleName xCertificate + + Node $AllNodes.NodeName + { + xCertificateExport SSLCert + { + Type = 'CERT' + FriendlyName = 'Web Site SSL Certificate for www.contoso.com' + Path = 'c:\sslcert.cer' + } + } +} +``` + +Exports a certificate as a PFX using the friendly name to identify it. + +```powershell +Configuration Example +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [PSCredential] + $Credential + ) -# Validate results -Get-ChildItem Cert:\LocalMachine\My + Import-DscResource -ModuleName xCertificate + + Node $AllNodes.NodeName + { + xCertificateExport SSLCert + { + Type = 'PFX' + FriendlyName = 'Web Site SSL Certificate for www.contoso.com' + Path = 'c:\sslcert.cer' + Password = $Credential + } + } +} ``` diff --git a/Tests/Integration/MSFT_xCertReq.Integration.Tests.ps1 b/Tests/Integration/MSFT_xCertReq.Integration.Tests.ps1 index c1c92449..8960b85a 100644 --- a/Tests/Integration/MSFT_xCertReq.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xCertReq.Integration.Tests.ps1 @@ -12,7 +12,7 @@ $script:DSCResourceName = 'MSFT_xCertReq' These tests can only be run if a CA is available and configured to be used on the computer running these tests. This is usually required to be a domain joined computer. #> -$CertUtilResult = & "$ENV:SystemRoot\system32\certutil.exe" @('-dump') +$CertUtilResult = & "$env:SystemRoot\system32\certutil.exe" @('-dump') $Result = ([regex]::matches($CertUtilResult,'Name:[ \t]+`([\sA-Za-z0-9._-]+)''','IgnoreCase')) if ([String]::IsNullOrEmpty($Result)) { @@ -48,7 +48,7 @@ try Describe "$($script:DSCResourceName)_Integration" { #region DEFAULT TESTS - It 'Should compile without throwing' { + It 'Should compile and apply the MOF without throwing' { { # This is to allow the testing of certreq with domain credentials $ConfigData = @{ @@ -64,12 +64,13 @@ try & "$($script:DSCResourceName)_Config" ` -OutputPath $TestDrive ` -ConfigurationData $ConfigData + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force - } | Should not throw + } | Should Not Throw } - It 'should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw } #endregion diff --git a/Tests/Integration/MSFT_xCertReq.config.ps1 b/Tests/Integration/MSFT_xCertReq.config.ps1 index e140c8fe..f1304a7d 100644 --- a/Tests/Integration/MSFT_xCertReq.config.ps1 +++ b/Tests/Integration/MSFT_xCertReq.config.ps1 @@ -1,5 +1,5 @@ # This will fail if the machine does not have a CA Configured. -$CertUtilResult = & "$ENV:SystemRoot\system32\certutil.exe" @('-dump') +$CertUtilResult = & "$env:SystemRoot\system32\certutil.exe" @('-dump') $CAServerFQDN = ([regex]::matches($CertUtilResult,'Server:[ \t]+`([A-Za-z0-9._-]+)''','IgnoreCase')).Groups[1].Value $CARootName = ([regex]::matches($CertUtilResult,'Name:[ \t]+`([\sA-Za-z0-9._-]+)''','IgnoreCase')).Groups[1].Value $KeyLength = '1024' diff --git a/Tests/Integration/MSFT_xCertificateExport.Integration.Tests.ps1 b/Tests/Integration/MSFT_xCertificateExport.Integration.Tests.ps1 new file mode 100644 index 00000000..5f4c23a2 --- /dev/null +++ b/Tests/Integration/MSFT_xCertificateExport.Integration.Tests.ps1 @@ -0,0 +1,172 @@ +$script:DSCModuleName = 'xCertificate' +$script:DSCResourceName = 'MSFT_xCertificateExport' + +#region HEADER +# Integration Test Template Version: 1.1.0 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration +#endregion + +Import-Module -Name (Join-Path -Path (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath 'TestHelpers') -ChildPath 'CommonTestHelper.psm1') -Global + +# Using try/finally to always cleanup even if something awful happens. +try +{ + #region Integration Tests + $ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName).config.ps1" + . $ConfigFile + + Describe "$($script:DSCResourceName)_Integration" { + # Download and dot source the New-SelfSignedCertificateEx script + . (Install-NewSelfSignedCertificateExScript) + + # Prepare CER certificate properties + $script:certificatePath = Join-Path -Path $env:Temp -ChildPath 'xCertificateExportTestCert.cer' + $null = Remove-Item -Path $script:certificatePath -Force -ErrorAction SilentlyContinue + + # Prepare PFX certificate properties + $script:pfxPath = Join-Path -Path $env:Temp -ChildPath 'xCertificateExportTestCert.pfx' + $null = Remove-Item -Path $script:pfxPath -Force -ErrorAction SilentlyContinue + $pfxPlainTextPassword = 'P@ssword!1' + $pfxPassword = ConvertTo-SecureString -String $pfxPlainTextPassword -AsPlainText -Force + $pfxCredential = New-Object -TypeName System.Management.Automation.PSCredential ` + -ArgumentList ('Dummy',$pfxPassword) + + # Generate the Valid certificate for testing + $certificateDNSNames = @('www.fabrikam.com', 'www.contoso.com') + $certificateKeyUsage = @('DigitalSignature','DataEncipherment') + $certificateEKU = @('Server Authentication','Client authentication') + $certificateSubject = 'CN=contoso, DC=com' + $certFriendlyName = 'Contoso Test Cert' + $validCertificate = New-SelfSignedCertificateEx ` + -Subject $certificateSubject ` + -KeyUsage $certificateKeyUsage ` + -KeySpec 'Exchange' ` + -EKU $certificateEKU ` + -SubjectAlternativeName $certificateDNSNames ` + -FriendlyName $certFriendlyName ` + -StoreLocation 'LocalMachine' ` + -Exportable + $script:validCertificateThumbprint = $validCertificate.Thumbprint + + Context 'Export CERT' { + #region DEFAULT TESTS + It 'Should compile and apply the MOF without throwing' { + { + # This is to allow the testing of certreq with domain credentials + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + Path = $script:certificatePath + FriendlyName = $certFriendlyName + Subject = $certificateSubject + DNSName = $certificateDNSNames + Issuer = $certificateSubject + KeyUsage = $certificateKeyUsage + EnhancedKeyUsage = $certificateEKU + MatchSource = $true + Type = 'CERT' + } + ) + } + + & "$($script:DSCResourceName)_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData + + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { $script:currentCertificate = Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + } + #endregion + + It 'Should have exported a Cert certificate' { + $script:currentCertificate.IsExported | Should Be $true + } + + It 'Should have set the resource and the thumbprint of the exported certificate should match' { + $exportedCertificate = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2Collection' + $exportedCertificate.Import($script:certificatePath) + $exportedCertificate[0].Thumbprint | Should Be $script:validCertificateThumbprint + } + } + + Context 'Export PFX' { + #region DEFAULT TESTS + It 'Should compile and apply the MOF without throwing' { + { + # This is to allow the testing of certreq with domain credentials + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + Path = $script:pfxPath + FriendlyName = $certFriendlyName + Subject = $certificateSubject + DNSName = $certificateDNSNames + Issuer = $certificateSubject + KeyUsage = $certificateKeyUsage + EnhancedKeyUsage = $certificateEKU + MatchSource = $true + Type = 'PFX' + ChainOption = 'BuildChain' + Password = $pfxCredential + PsDscAllowPlainTextPassword = $true + } + ) + } + + & "$($script:DSCResourceName)_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData + + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { $script:currentPFX = Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + } + #endregion + + It 'Should have exported a PFX certificate' { + $script:currentPFX.IsExported | Should Be $true + } + + It 'Should have set the resource and the thumbprint of the exported certificate should match' { + $exportedCertificate = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2Collection' + $exportedCertificate.Import($script:certificatePath,$pfxPassword,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) + $exportedCertificate[0].Thumbprint | Should Be $script:validCertificateThumbprint + } + } + + AfterAll { + # Cleanup + $validCertificate = Get-Item -Path "cert:\LocalMachine\My\$($script:validCertificateThumbprint)" + $null = Remove-Item -Path $validCertificate.PSPath -Force -ErrorAction SilentlyContinue + $null = Remove-Item -Path $script:pfxPath -Force -ErrorAction SilentlyContinue + $null = Remove-Item -Path $script:certificatePath -Force -ErrorAction SilentlyContinue + } + } + #endregion +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/Tests/Integration/MSFT_xCertificateExport.config.ps1 b/Tests/Integration/MSFT_xCertificateExport.config.ps1 new file mode 100644 index 00000000..431b9cdd --- /dev/null +++ b/Tests/Integration/MSFT_xCertificateExport.config.ps1 @@ -0,0 +1,35 @@ +Configuration MSFT_xCertificateExport_Config { + Import-DscResource -ModuleName xCertificate + node localhost { + if ($Node.Type -in @('Cert','P7B','SST')) + { + xCertificateExport Integration_Test { + Path = $Node.Path + FriendlyName = $Node.FriendlyName + Subject = $Node.Subject + DNSName = $node.DNSName + Issuer = $Node.Issuer + KeyUsage = $Node.KeyUsage + EnhancedKeyUsage = $Node.EnhancedKeyUsage + Type = $Node.Type + MatchSource = $Node.MatchSource + } + } + elseif ($Node.Type -eq 'PFX') + { + xCertificateExport Integration_Test { + Path = $Node.Path + FriendlyName = $Node.FriendlyName + Subject = $Node.Subject + DNSName = $node.DNSName + Issuer = $Node.Issuer + KeyUsage = $Node.KeyUsage + EnhancedKeyUsage = $Node.EnhancedKeyUsage + Type = $Node.Type + MatchSource = $Node.MatchSource + ChainOption = 'BuildChain' + Password = $Node.Password + } + } + } +} diff --git a/Tests/Integration/MSFT_xCertificateImport.Integration.Tests.ps1 b/Tests/Integration/MSFT_xCertificateImport.Integration.Tests.ps1 index faead442..e6b496dd 100644 --- a/Tests/Integration/MSFT_xCertificateImport.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xCertificateImport.Integration.Tests.ps1 @@ -25,10 +25,10 @@ try # Don't use CurrentUser certificates for this test because they won't be found because # DSC LCM runs under a different context (Local System). $Certificate = New-SelfSignedCertificate ` - -DnsName $ENV:ComputerName ` + -DnsName $env:ComputerName ` -CertStoreLocation Cert:\LocalMachine\My $CertificatePath = Join-Path ` - -Path $ENV:Temp ` + -Path $env:Temp ` -ChildPath "xCertificateImport-$($Certificate.Thumbprint).cer" $null = Export-Certificate ` -Cert $Certificate ` @@ -44,18 +44,19 @@ try Describe "$($script:DSCResourceName)_Add_Integration" { #region DEFAULT TESTS - It 'Should compile without throwing' { + It 'Should compile and apply the MOF without throwing' { { & "$($script:DSCResourceName)_Add_Config" ` -OutputPath $TestDrive ` -Path $CertificatePath ` -Thumbprint $Certificate.Thumbprint + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force - } | Should not throw + } | Should Not Throw } - It 'should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw } #endregion diff --git a/Tests/Integration/MSFT_xPfxImport.Integration.Tests.ps1 b/Tests/Integration/MSFT_xPfxImport.Integration.Tests.ps1 index 0383eae1..227b5e3b 100644 --- a/Tests/Integration/MSFT_xPfxImport.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xPfxImport.Integration.Tests.ps1 @@ -28,10 +28,10 @@ try # Don't use CurrentUser certificates for this test because they won't be found because # DSC LCM runs under a different context (Local System). $Certificate = New-SelfSignedCertificate ` - -DnsName $ENV:ComputerName ` + -DnsName $env:ComputerName ` -CertStoreLocation Cert:\LocalMachine\My $CertificatePath = Join-Path ` - -Path $ENV:Temp ` + -Path $env:Temp ` -ChildPath "xPfxImport-$($Certificate.Thumbprint).pfx" $testUsername = 'DummyUsername' $testPassword = 'DummyPassword' @@ -50,7 +50,7 @@ try Describe "$($script:DSCResourceName)_Add_Integration" { #region DEFAULT TESTS - It 'Should compile without throwing' { + It 'Should compile and apply the MOF without throwing' { { $configData = @{ AllNodes = @( @@ -66,12 +66,13 @@ try -Path $CertificatePath ` -Thumbprint $Certificate.Thumbprint ` -Credential $testCredential + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force - } | Should not throw + } | Should Not Throw } - It 'should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw } #endregion @@ -98,12 +99,13 @@ try -OutputPath $TestDrive ` -Path $CertificatePath ` -Thumbprint $Certificate.Thumbprint + Start-DscConfiguration -Path $TestDrive -ComputerName localhost -Wait -Verbose -Force - } | Should not throw + } | Should Not Throw } - It 'should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw } #endregion diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 new file mode 100644 index 00000000..ad6cce38 --- /dev/null +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -0,0 +1,135 @@ +<# + .SYNOPSIS + Returns an invalid argument exception object + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown +#> +function Get-InvalidArgumentRecord +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' -ArgumentList @( $Message, + $ArgumentName ) + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $argumentException, $ArgumentName, 'InvalidArgument', $null ) + } + return New-Object @newObjectParams +} + +<# + .SYNOPSIS + Returns an invalid operation exception object + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error +#> +function Get-InvalidOperationRecord +{ + [CmdletBinding()] + param + ( + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $Message) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' + } + elseif ($null -eq $ErrorRecord) + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message ) + } + else + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message, + $ErrorRecord.Exception ) + } + + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $invalidOperationException.ToString(), 'MachineStateIncorrect', + 'InvalidOperation', $null ) + } + return New-Object @newObjectParams +} + +<# + .SYNOPSIS + Some tests require a self-signed certificate to be created. However, the + New-SelfSignedCertificate cmdlet built into Windows Server 2012 R2 is too + limited to work for this process. + + Therefore an alternate method of creating self-signed certificates to meet the + reqirements. A script on Microsoft Script Center can be used for this but must + be downloaded: + https://gallery.technet.microsoft.com/scriptcenter/Self-signed-certificate-5920a7c6 + + This cmdlet will install the script if it is not available and dot source it. + + .PARAMETER OutputPath + The path to download the script to. If not provided will default to the current + users temp folder. + + .OUTPUTS + The path to the script that was downloaded. +#> +function Install-NewSelfSignedCertificateExScript +{ + [CmdletBinding()] + [OutputType([String])] + param + ( + [Parameter()] + [String] + $OutputPath = $env:Temp + ) + + $newSelfSignedCertURL = 'https://gallery.technet.microsoft.com/scriptcenter/Self-signed-certificate-5920a7c6/file/101251/2/New-SelfSignedCertificateEx.zip' + $newSelfSignedCertZip = Split-Path -Path $newSelfSignedCertURL -Leaf + $newSelfSignedCertZipPath = Join-Path -Path $OutputPath -ChildPath $newSelfSignedCertZip + $newSelfSignedCertScriptPath = Join-Path -Path $OutputPath -ChildPath 'New-SelfSignedCertificateEx.ps1' + if (-not (Test-Path -Path $newSelfSignedCertScriptPath)) + { + if (Test-Path -Path $newSelfSignedCertZip) + { + Remove-Item -Path $newSelfSignedCertZipPath -Force + } + Invoke-WebRequest -Uri $newSelfSignedCertURL -OutFile $newSelfSignedCertZipPath + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($newSelfSignedCertZipPath, $OutputPath) + } # if + return $newSelfSignedCertScriptPath +} # end function Install-NewSelfSignedCertificateExScript + +Export-ModuleMember -Function ` + Install-NewSelfSignedCertificateExScript, ` + Get-InvalidArgumentRecord, ` + Get-InvalidOperationRecord diff --git a/Tests/Unit/CertificateCommon.tests.ps1 b/Tests/Unit/CertificateCommon.tests.ps1 deleted file mode 100644 index 93b6d7b1..00000000 --- a/Tests/Unit/CertificateCommon.tests.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -$script:DSCModuleName = 'xCertificate' -$script:DSCResourceName = 'CertificateCommon' - -#region HEADER -# Integration Test Template Version: 1.1.0 -[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) -if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) -{ - & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) -} - -Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force -$TestEnvironment = Initialize-TestEnvironment ` - -DSCModuleName $script:DSCModuleName ` - -DSCResourceName $script:DSCResourceName ` - -TestType Unit -#endregion - -# Begin Testing -try -{ - InModuleScope $script:DSCResourceName { - $DSCResourceName = 'CertificateCommon' - $invalidThumbprint = 'Zebra' - $validThumbprint = ( - [System.AppDomain]::CurrentDomain.GetAssemblies().GetTypes() | Where-Object { - $_.BaseType.BaseType -eq [System.Security.Cryptography.HashAlgorithm] -and - ($_.Name -cmatch 'Managed$' -or $_.Name -cmatch 'Provider$') - } | Select-Object -First 1 | ForEach-Object { - (New-Object $_).ComputeHash([String]::Empty) | ForEach-Object { - '{0:x2}' -f $_ - } - } - ) -join '' - - $testFile = 'test.pfx' - - $invalidPath = 'TestDrive:' - $validPath = "TestDrive:\$testFile" - - Describe "$DSCResourceName\Test-CertificatePath" { - - $null | Set-Content -Path $validPath - - Context 'a single existing file by parameter' { - $result = Test-CertificatePath -Path $validPath - It 'should return true' { - ($result -is [bool]) | Should Be $true - $result | Should Be $true - } - } - Context 'a single missing file by parameter' { - It 'should throw an exception' { - # directories are not valid - { Test-CertificatePath -Path $invalidPath } | Should Throw - } - } - Context 'a single missing file by parameter with -Quiet' { - $result = Test-CertificatePath -Path $invalidPath -Quiet - It 'should return false' { - ($result -is [bool]) | Should Be $true - $result | Should Be $false - } - } - Context 'a single existing file by pipeline' { - $result = $validPath | Test-CertificatePath - It 'should return true' { - ($result -is [bool]) | Should Be $true - $result | Should Be $true - } - } - Context 'a single missing file by pipeline' { - It 'should throw an exception' { - # directories are not valid - { $invalidPath | Test-CertificatePath } | Should Throw - } - } - Context 'a single missing file by pipeline with -Quiet' { - $result = $invalidPath | Test-CertificatePath -Quiet - It 'should return false' { - ($result -is [bool]) | Should Be $true - $result | Should Be $false - } - } - } - Describe "$DSCResourceName\Test-Thumbprint" { - - Context 'a single valid thumbrpint by parameter' { - $result = Test-Thumbprint -Thumbprint $validThumbprint - It 'should return true' { - ($result -is [bool]) | Should Be $true - $result | Should Be $true - } - } - Context 'a single invalid thumbprint by parameter' { - It 'should throw an exception' { - # directories are not valid - { Test-Thumbprint -Thumbprint $invalidThumbprint } | Should Throw - } - } - Context 'a single invalid thumbprint by parameter with -Quiet' { - $result = Test-Thumbprint $invalidThumbprint -Quiet - It 'should return false' { - ($result -is [bool]) | Should Be $true - $result | Should Be $false - } - } - Context 'a single valid thumbprint by pipeline' { - $result = $validThumbprint | Test-Thumbprint - It 'should return true' { - ($result -is [bool]) | Should Be $true - $result | Should Be $true - } - } - Context 'a single invalid thumborint by pipeline' { - It 'should throw an exception' { - # directories are not valid - { $invalidThumbprint | Test-Thumbprint } | Should Throw - } - } - Context 'a single invalid thumbprint by pipeline with -Quiet' { - $result = $invalidThumbprint | Test-Thumbprint -Quiet - It 'should return false' { - ($result -is [bool]) | Should Be $true - $result | Should Be $false - } - } - } - } -} -finally -{ - #region FOOTER - Restore-TestEnvironment -TestEnvironment $TestEnvironment - #endregion -} diff --git a/Tests/Unit/CertificateDsc.Common.Tests.ps1 b/Tests/Unit/CertificateDsc.Common.Tests.ps1 new file mode 100644 index 00000000..4ca15c4f --- /dev/null +++ b/Tests/Unit/CertificateDsc.Common.Tests.ps1 @@ -0,0 +1,596 @@ +$script:ModuleName = 'CertificateDsc.Common' + +#region HEADER +# Unit Test Template Version: 1.1.0 +[string] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +Import-Module -Name (Join-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'Modules' -ChildPath $script:ModuleName)) -ChildPath "$script:ModuleName.psm1") -Force +Import-Module -Name (Join-Path -Path (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath 'TestHelpers') -ChildPath 'CommonTestHelper.psm1') -Global +#endregion HEADER + +# Begin Testing +try +{ + InModuleScope $script:ModuleName { + $DSCResourceName = 'CertificateDsc.Common' + $invalidThumbprint = 'Zebra' + $validThumbprint = ( + [System.AppDomain]::CurrentDomain.GetAssemblies().GetTypes() | Where-Object { + $_.BaseType.BaseType -eq [System.Security.Cryptography.HashAlgorithm] -and + ($_.Name -cmatch 'Managed$' -or $_.Name -cmatch 'Provider$') + } | Select-Object -First 1 | ForEach-Object { + (New-Object $_).ComputeHash([String]::Empty) | ForEach-Object { + '{0:x2}' -f $_ + } + } + ) -join '' + + $testFile = 'test.pfx' + + $invalidPath = 'TestDrive:' + $validPath = "TestDrive:\$testFile" + + Describe "$DSCResourceName\Test-CertificatePath" { + + $null | Set-Content -Path $validPath + + Context 'a single existing file by parameter' { + $result = Test-CertificatePath -Path $validPath + It 'should return true' { + ($result -is [bool]) | Should Be $true + $result | Should Be $true + } + } + + Context 'a single missing file by parameter' { + It 'should throw an exception' { + # directories are not valid + { Test-CertificatePath -Path $invalidPath } | Should Throw + } + } + + Context 'a single missing file by parameter with -Quiet' { + $result = Test-CertificatePath -Path $invalidPath -Quiet + It 'should return false' { + ($result -is [bool]) | Should Be $true + $result | Should Be $false + } + } + + Context 'a single existing file by pipeline' { + $result = $validPath | Test-CertificatePath + It 'should return true' { + ($result -is [bool]) | Should Be $true + $result | Should Be $true + } + } + + Context 'a single missing file by pipeline' { + It 'should throw an exception' { + # directories are not valid + { $invalidPath | Test-CertificatePath } | Should Throw + } + } + + Context 'a single missing file by pipeline with -Quiet' { + $result = $invalidPath | Test-CertificatePath -Quiet + It 'should return false' { + ($result -is [bool]) | Should Be $true + $result | Should Be $false + } + } + } + + Describe "$DSCResourceName\Test-Thumbprint" { + + Context 'a single valid thumbrpint by parameter' { + $result = Test-Thumbprint -Thumbprint $validThumbprint + It 'should return true' { + ($result -is [bool]) | Should Be $true + $result | Should Be $true + } + } + + Context 'a single invalid thumbprint by parameter' { + It 'should throw an exception' { + # directories are not valid + { Test-Thumbprint -Thumbprint $invalidThumbprint } | Should Throw + } + } + + Context 'a single invalid thumbprint by parameter with -Quiet' { + $result = Test-Thumbprint $invalidThumbprint -Quiet + It 'should return false' { + ($result -is [bool]) | Should Be $true + $result | Should Be $false + } + } + + Context 'a single valid thumbprint by pipeline' { + $result = $validThumbprint | Test-Thumbprint + It 'should return true' { + ($result -is [bool]) | Should Be $true + $result | Should Be $true + } + } + + Context 'a single invalid thumborint by pipeline' { + It 'should throw an exception' { + # directories are not valid + { $invalidThumbprint | Test-Thumbprint } | Should Throw + } + } + + Context 'a single invalid thumbprint by pipeline with -Quiet' { + $result = $invalidThumbprint | Test-Thumbprint -Quiet + It 'should return false' { + ($result -is [bool]) | Should Be $true + $result | Should Be $false + } + } + } + + Describe "$DSCResourceName\Find-Certificate" { + + # Download and dot source the New-SelfSignedCertificateEx script + . (Install-NewSelfSignedCertificateExScript) + + # Generate the Valid certificate for testing but remove it from the store straight away + $certDNSNames = @('www.fabrikam.com', 'www.contoso.com') + $certDNSNamesReverse = @('www.contoso.com', 'www.fabrikam.com') + $certDNSNamesNoMatch = $certDNSNames + @('www.nothere.com') + $certKeyUsage = @('DigitalSignature','DataEncipherment') + $certKeyUsageReverse = @('DataEncipherment','DigitalSignature') + $certKeyUsageNoMatch = $certKeyUsage + @('KeyEncipherment') + $certEKU = @('Server Authentication','Client authentication') + $certEKUReverse = @('Client authentication','Server Authentication') + $certEKUNoMatch = $certEKU + @('Encrypting File System') + $certSubject = 'CN=contoso, DC=com' + $certFriendlyName = 'Contoso Test Cert' + $validCert = New-SelfSignedCertificateEx ` + -Subject $certSubject ` + -KeyUsage $certKeyUsage ` + -KeySpec 'Exchange' ` + -EKU $certEKU ` + -SubjectAlternativeName $certDNSNames ` + -FriendlyName $certFriendlyName ` + -StoreLocation 'CurrentUser' ` + -Exportable + # Pull the generated certificate from the store so we have the friendlyname + $validThumbprint = $validCert.Thumbprint + $validCert = Get-Item -Path "cert:\CurrentUser\My\$validThumbprint" + Remove-Item -Path $validCert.PSPath -Force + + # Generate the Expired certificate for testing but remove it from the store straight away + $expiredCert = New-SelfSignedCertificateEx ` + -Subject $certSubject ` + -KeyUsage $certKeyUsage ` + -KeySpec 'Exchange' ` + -EKU $certEKU ` + -SubjectAlternativeName $certDNSNames ` + -FriendlyName $certFriendlyName ` + -NotBefore ((Get-Date) - (New-TimeSpan -Days 2)) ` + -NotAfter ((Get-Date) - (New-TimeSpan -Days 1)) ` + -StoreLocation 'CurrentUser' ` + -Exportable + # Pull the generated certificate from the store so we have the friendlyname + $expiredThumbprint = $expiredCert.Thumbprint + $expiredCert = Get-Item -Path "cert:\CurrentUser\My\$expiredThumbprint" + Remove-Item -Path $expiredCert.PSPath -Force + + $nocertThumbprint = '1111111111111111111111111111111111111111' + + # Dynamic mock content for Get-ChildItem + $mockGetChildItem = { + switch ( $Path ) + { + 'cert:\LocalMachine\My' + { + return @( $validCert ) + } + + 'cert:\LocalMachine\NoCert' + { + return @() + } + + 'cert:\LocalMachine\TwoCerts' + { + return @( $expiredCert, $validCert ) + } + + 'cert:\LocalMachine\Expired' + { + return @( $expiredCert ) + } + + default + { + throw 'mock called with unexpected value {0}' -f $Path + } + } + } + + BeforeEach { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName Get-ChildItem ` + -MockWith $mockGetChildItem + } + + Context 'Thumbprint only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $nocertThumbprint } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName 'Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Subject only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Subject $certSubject } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Subject only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Subject 'CN=Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Issuer only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Issuer $certSubject } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Issuer only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Issuer 'CN=Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNames } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNamesReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNames[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNamesNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsage } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsageReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsage[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsageNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKU } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKUReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKU[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKUNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist in the store' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint -Store 'NoCert'} | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and both valid and expired certificates exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'TwoCerts' } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and only expired certificates exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'Expired' } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and only expired certificates exist but allowexpired passed' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'Expired' -AllowExpired:$true } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $expiredThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + } + } +} +finally +{ + #region FOOTER + #endregion +} diff --git a/Tests/Unit/MSFT_xCertReq.tests.ps1 b/Tests/Unit/MSFT_xCertReq.Tests.ps1 similarity index 99% rename from Tests/Unit/MSFT_xCertReq.tests.ps1 rename to Tests/Unit/MSFT_xCertReq.Tests.ps1 index d41aa654..6b367e4b 100644 --- a/Tests/Unit/MSFT_xCertReq.tests.ps1 +++ b/Tests/Unit/MSFT_xCertReq.Tests.ps1 @@ -282,7 +282,7 @@ RenewalCert = $validThumbprint #region Set-TargetResource Describe "$DSCResourceName\Set-TargetResource" { Mock -CommandName Join-Path -MockWith { 'xCertReq-Test' } ` - -ParameterFilter { $Path -eq $ENV:Temp } + -ParameterFilter { $Path -eq $env:Temp } Mock -CommandName Test-Path -MockWith { $true } ` -ParameterFilter { $Path -eq 'xCertReq-Test.req' } Mock -CommandName Test-Path -MockWith { $true } ` diff --git a/Tests/Unit/MSFT_xCertificateExport.Tests.ps1 b/Tests/Unit/MSFT_xCertificateExport.Tests.ps1 new file mode 100644 index 00000000..49d7597e --- /dev/null +++ b/Tests/Unit/MSFT_xCertificateExport.Tests.ps1 @@ -0,0 +1,436 @@ +$script:DSCModuleName = 'xCertificate' +$script:DSCResourceName = 'MSFT_xCertificateExport' + +#region HEADER +# Integration Test Template Version: 1.1.0 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit +#endregion + +# Begin Testing +try +{ + InModuleScope $script:DSCResourceName { + $DSCResourceName = 'MSFT_xCertificateExport' + + $certificatePath = Join-Path -Path $env:Temp -ChildPath 'xCertificateExportTestCert.cer' + $pfxPath = Join-Path -Path $env:Temp -ChildPath 'xCertificateExportTestCert.pfx' + $certificateDNSNames = @('www.fabrikam.com', 'www.contoso.com') + $certificateKeyUsage = @('DigitalSignature','DataEncipherment') + $certificateEKU = @('Server Authentication','Client authentication') + $certificateSubject = 'CN=contoso, DC=com' + $certificateFriendlyName = 'Contoso Test Cert' + $certificateThumbprint = '1111111111111111111111111111111111111111' + $certificateNotFoundThumbprint = '2222222222222222222222222222222222222222' + $certificateStore = 'My' + + $validCertificate = New-Object -TypeName PSObject -Property @{ + Thumbprint = $certificateThumbprint + Subject = "CN=$certificateSubject" + Issuer = "CN=$certificateSubject" + FriendlyName = $certificateFriendlyName + DnsNameList = @( + @{ Unicode = $certificateDNSNames[0] } + @{ Unicode = $certificateDNSNames[1] } + ) + Extensions = @( + @{ EnhancedKeyUsages = ($certificateKeyUsage -join ', ') } + ) + EnhancedKeyUsages = @( + @{ FriendlyName = $certificateEKU[0] } + @{ FriendlyName = $certificateEKU[1] } + ) + NotBefore = (Get-Date).AddDays(-30) # Issued on + NotAfter = (Get-Date).AddDays(31) # Expires after + } + + $validCertificateParameters = @{ + Path = $certificatePath + Thumbprint = $certificateThumbprint + FriendlyName = $certificateFriendlyName + Subject = $certificateSubject + DNSName = $certificateDNSNames + Issuer = $certificateSubject + KeyUsage = $certificateKeyUsage + EnhancedKeyUsage = $certificateEKU + Store = $certificateStore + AllowExpired = $false + MatchSource = $false + Type = 'Cert' + } + + $validCertificateNotFoundParameters = @{} + $validCertificateParameters + $validCertificateNotFoundParameters.Thumbprint = $certificateNotFoundThumbprint + + $validCertificateMatchSourceParameters = @{} + $validCertificateParameters + $validCertificateMatchSourceParameters.MatchSource = $true + + $pfxPlainTextPassword = 'P@ssword!1' + $pfxPassword = ConvertTo-SecureString -String $pfxPlainTextPassword -AsPlainText -Force + $pfxCredential = New-Object -TypeName System.Management.Automation.PSCredential ` + -ArgumentList ('Dummy',$pfxPassword) + + $validPfxParameters = @{ + Path = $PfxPath + Thumbprint = $certificateThumbprint + FriendlyName = $certificateFriendlyName + Subject = $certificateSubject + DNSName = $certificateDNSNames + Issuer = $certificateSubject + KeyUsage = $certificateKeyUsage + EnhancedKeyUsage = $certificateEKU + Store = $certificateStore + AllowExpired = $false + MatchSource = $false + Password = $pfxCredential + ProtectTo = 'Administrators' + Type = 'PFX' + } + + $validPfxMatchSourceParameters = @{} + $validPfxParameters + $validPfxMatchSourceParameters.MatchSource = $true + + # This is so we can mock the Import method in Set-TargetResource + class X509Certificate2CollectionDummyMatch:System.Object + { + [String] $Thumbprint = '1111111111111111111111111111111111111111' + X509Certificate2CollectionDummyMatch() + { + } + Import($Path) + { + } + Import($Path,$Password,$Flags) + { + } + } + + class X509Certificate2CollectionDummyNoMatch:System.Object + { + [String] $Thumbprint = '2222222222222222222222222222222222222222' + X509Certificate2CollectionDummyNoMatch() + { + } + Import($Path) + { + } + Import($Path,$Password,$Flags) + { + } + } + + $importedCertificateMatch = New-Object -Type X509Certificate2CollectionDummyMatch + $importedCertificateNoMatch = New-Object -Type X509Certificate2CollectionDummyNoMatch + + # MockWith content for Export-Certificate + $mockExportCertificate = { + if ($FilePath -ne $certificatePath) + { + throw 'Expected mock to be called with {0}, but was {1}' -f $certificatePath,$FilePath + } + } + + # MockWith content for Export-PfxCertificate + $mockExportPfxCertificate = { + if ($FilePath -ne $pfxPath) + { + throw 'Expected mock to be called with {0}, but was {1}' -f $pfxPath,$FilePath + } + } + + # MockWith content for Find-Certifciate + $mockFindCertificate = { + if ($Thumbprint -eq $certificateThumbprint) + { + $validCertificate + } + } + + Describe "$DSCResourceName\Get-TargetResource" { + Context 'Certificate has been exported' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } ` + -ParameterFilter { $Path -eq $certificatePath } + + It 'should return IsExported true' { + $Result = Get-TargetResource -Path $certificatePath -Verbose + $Result.IsExported | Should Be $true + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + } + } + + Context 'Certificate has not been exported' { + Mock ` + -CommandName Test-Path ` + -MockWith { $false } ` + -ParameterFilter { $Path -eq $certificatePath } + + It 'should return IsExported false' { + $Result = Get-TargetResource -Path $certificatePath -Verbose + $Result.IsExported | Should Be $false + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + } + } + } + + Describe "$DSCResourceName\Set-TargetResource" { + BeforeEach { + Mock ` + -CommandName Find-Certificate ` + -MockWith $mockFindCertificate + } + + Context 'Certificate is not found' { + Mock ` + -CommandName Export-Certificate + + Mock ` + -CommandName Export-PfxCertificate + + It 'should not throw exception' { + { Set-TargetResource @validCertificateNotFoundParameters -Verbose } | Should Not Throw + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Export-Certificate -Exactly -Times 0 + Assert-MockCalled -CommandName Export-PfxCertificate -Exactly -Times 0 + } + } + + Context 'Certificate is found and needs to be exported as Cert' { + # Needs to be done because real Export-Certificate $cert parameter requires an actual [X509Certificate2] object + function Export-Certificate + { + [CmdletBinding()] + param + ( + $FilePath, + $Cert, + $Force, + $Type + ) + } + + Mock ` + -CommandName Export-Certificate ` + -MockWith $mockExportCertificate + + Mock ` + -CommandName Export-PfxCertificate + + It 'should not throw exception' { + { Set-TargetResource @validCertificateParameters -Verbose } | Should Not Throw + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Export-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Export-PfxCertificate -Exactly -Times 0 + } + } + + Context 'Certificate is found and needs to be exported as PFX' { + # Needs to be done because real Export-PfxCertificate $cert parameter requires an actual [X509Certificate2] object + function Export-PfxCertificate + { + [CmdletBinding()] + param + ( + $FilePath, + $Cert, + $Force, + $Type, + $Password, + $ChainOption, + $ProtectTo + ) + } + + Mock ` + -CommandName Export-Certificate + + Mock ` + -CommandName Export-PfxCertificate ` + -MockWith $mockExportPfxCertificate + + It 'should not throw exception' { + { Set-TargetResource @validPfxParameters -Verbose } | Should Not Throw + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Export-Certificate -Exactly -Times 0 + Assert-MockCalled -CommandName Export-PfxCertificate -Exactly -Times 1 + } + } + } + + Describe "$DSCResourceName\Test-TargetResource" { + BeforeEach { + Mock ` + -CommandName Find-Certificate ` + -MockWith $mockFindCertificate + } + + Context 'Certificate is not found' { + Mock ` + -CommandName Test-Path + + Mock ` + -CommandName New-Object + + It 'should return true' { + Test-TargetResource @validCertificateNotFoundParameters -Verbose | Should Be $true + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 0 + Assert-MockCalled -CommandName New-Object -Exactly -Times 0 + } + } + + Context 'Certificate is found and needs to be exported as Cert and has not been exported' { + Mock ` + -CommandName Test-Path ` + -MockWith { $false } + + Mock ` + -CommandName New-Object + + It 'should return false' { + Test-TargetResource @validCertificateParameters -Verbose | Should Be $false + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 0 + } + } + + Context 'Certificate is found and needs to be exported as Cert but already exported and MatchSource False' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName New-Object + + It 'should return true' { + Test-TargetResource @validCertificateParameters -Verbose | Should Be $true + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 0 + } + } + + Context 'Certificate is found and needs to be exported as Cert but already exported and MatchSource True and matches' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName New-Object ` + -MockWith { $importedCertificateMatch } + + It 'should return true' { + Test-TargetResource @validCertificateMatchSourceParameters -Verbose | Should Be $true + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 1 + } + } + + Context 'Certificate is found and needs to be exported as Cert but already exported and MatchSource True but no match' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName New-Object ` + -MockWith { $importedCertificateNoMatch } + + It 'should return false' { + Test-TargetResource @validCertificateMatchSourceParameters -Verbose | Should Be $false + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 1 + } + } + + Context 'Certificate is found and needs to be exported as Pfx but already exported and MatchSource True and matches' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName New-Object ` + -MockWith { $importedCertificateMatch } + + It 'should return true' { + Test-TargetResource @validPfxMatchSourceParameters -Verbose | Should Be $true + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 1 + } + } + + Context 'Certificate is found and needs to be exported as Pfx but already exported and MatchSource True but no match' { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName New-Object ` + -MockWith { $importedCertificateNoMatch } + + It 'should return false' { + Test-TargetResource @validPfxMatchSourceParameters -Verbose | Should Be $false + } + + It 'should call the expected mocks' { + Assert-MockCalled -CommandName Find-Certificate -Exactly -Times 1 + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName New-Object -Exactly -Times 1 + } + } + } + } +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/Tests/Unit/MSFT_xCertificateImport.tests.ps1 b/Tests/Unit/MSFT_xCertificateImport.Tests.ps1 similarity index 100% rename from Tests/Unit/MSFT_xCertificateImport.tests.ps1 rename to Tests/Unit/MSFT_xCertificateImport.Tests.ps1 diff --git a/Tests/Unit/MSFT_xPfxImport.tests.ps1 b/Tests/Unit/MSFT_xPfxImport.Tests.ps1 similarity index 100% rename from Tests/Unit/MSFT_xPfxImport.tests.ps1 rename to Tests/Unit/MSFT_xPfxImport.Tests.ps1 diff --git a/appveyor.yml b/appveyor.yml index fa817d60..c4cbc9a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,63 +1,34 @@ -#---------------------------------# -# environment configuration # -#---------------------------------# +#---------------------------------# +# environment configuration # +#---------------------------------# version: 2.3.{build}.0 -install: +install: - git clone https://github.com/PowerShell/DscResource.Tests + - ps: | - Import-Module -Name .\DscResource.Tests\TestHelper.psm1 -Force - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force - Install-Module -Name Pester -Repository PSGallery -Force + Import-Module "$env:APPVEYOR_BUILD_FOLDER\DscResource.Tests\AppVeyor.psm1" + Invoke-AppveyorInstallTask -#---------------------------------# -# build configuration # -#---------------------------------# +#---------------------------------# +# build configuration # +#---------------------------------# build: false -#---------------------------------# -# test configuration # -#---------------------------------# +#---------------------------------# +# test configuration # +#---------------------------------# test_script: - ps: | - $testResultsFile = ".\TestsResults.xml" - $res = Invoke-Pester -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru - (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) - if ($res.FailedCount -gt 0) { - throw "$($res.FailedCount) tests failed." - } - -#---------------------------------# -# deployment configuration # -#---------------------------------# - -# scripts to run before deployment -deploy_script: - - ps: | - # Creating project artifact - $stagingDirectory = (Resolve-Path ..).Path - $manifest = Join-Path $pwd "xCertificate.psd1" - (Get-Content $manifest -Raw).Replace("2.3.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest - $zipFilePath = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" - Add-Type -assemblyname System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::CreateFromDirectory($pwd, $zipFilePath) - - # Creating NuGet package artifact - New-Nuspec -packageName $env:APPVEYOR_PROJECT_NAME -version $env:APPVEYOR_BUILD_VERSION -author "Microsoft" -owners "Microsoft" -licenseUrl "https://github.com/PowerShell/DscResources/blob/master/LICENSE" -projectUrl "https://github.com/$($env:APPVEYOR_REPO_NAME)" -packageDescription $env:APPVEYOR_PROJECT_NAME -tags "DesiredStateConfiguration DSC DSCResourceKit" -destinationPath . - nuget pack ".\$($env:APPVEYOR_PROJECT_NAME).nuspec" -outputdirectory . - $nuGetPackageName = $env:APPVEYOR_PROJECT_NAME + "." + $env:APPVEYOR_BUILD_VERSION + ".nupkg" - $nuGetPackagePath = (Get-ChildItem $nuGetPackageName).FullName - - @( - # You can add other artifacts here - $zipFilePath, - $nuGetPackagePath - ) | % { - Write-Host "Pushing package $_ as Appveyor artifact" - Push-AppveyorArtifact $_ - } - - + Invoke-AppveyorTestScriptTask ` + -CodeCoverage +#---------------------------------# +# deployment configuration # +#---------------------------------# +# scripts to run before deployment +deploy_script: + - ps: | + Invoke-AppveyorAfterTestTask diff --git a/xCertificate.psd1 b/xCertificate.psd1 index 2b89d573..45e6387d 100644 --- a/xCertificate.psd1 +++ b/xCertificate.psd1 @@ -1,125 +1,65 @@ -# -# Module manifest for module 'xCertificate' -# -# Generated by: PowerShell DSC -# -# Generated on: 4/8/2015 -# - @{ + # Version number of this module. + ModuleVersion = '2.3.0.0' -# Script module or binary module file associated with this manifest. -# RootModule = '' - -# Version number of this module. -ModuleVersion = '2.3.0.0' - -# ID used to uniquely identify this module -GUID = '1b8d785e-79ae-4d95-ae58-b2460aec1031' - -# Author of this module -Author = 'Microsoft Corporation' - -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' - -# Copyright statement for this module -Copyright = '(c) 2015 Microsoft Corporation. All rights reserved.' - -# Description of the functionality provided by this module -Description = 'This module includes DSC resources that simplify administration of certificates on a Windows Server' - -# Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '4.0' - -# Name of the Windows PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the Windows PowerShell host required by this module -# PowerShellHostVersion = '' + # ID used to uniquely identify this module + GUID = '1b8d785e-79ae-4d95-ae58-b2460aec1031' -# Minimum version of Microsoft .NET Framework required by this module -# DotNetFrameworkVersion = '' + # Author of this module + Author = 'Microsoft Corporation' -# Minimum version of the common language runtime (CLR) required by this module -# CLRVersion = '' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Copyright statement for this module + Copyright = '(c) 2015 Microsoft Corporation. All rights reserved.' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # Description of the functionality provided by this module + Description = 'This module includes DSC resources that simplify administration of certificates on a Windows Server' -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @('Modules\CertificateDsc.Common\CertificateDsc.Common.psm1','Modules\CertificateDsc.ResourceHelper\CertificateDsc.ResourceHelper.psm1','Modules\CertificateDsc.PDT\CertificateDsc.PDT.psm1') -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Functions to export from this module + FunctionsToExport = '*' -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Cmdlets to export from this module + CmdletsToExport = '*' -# Functions to export from this module -FunctionsToExport = '*' + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ -# Cmdlets to export from this module -CmdletsToExport = '*' + PSData = @{ -# Variables to export from this module -VariablesToExport = '*' + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') -# Aliases to export from this module -AliasesToExport = '*' + # A URL to the license for this module. + LicenseUri = 'https://github.com/PowerShell/xCertificate/blob/master/LICENSE' -# DSC resources to export from this module -# DscResourcesToExport = @() + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShell/xCertificate' -# List of all modules packaged with this module -# ModuleList = @() + # A URL to an icon representing this module. + # IconUri = '' -# List of all files packaged with this module -# FileList = @() - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/PowerShell/xCertificate/blob/master/LICENSE' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/PowerShell/xCertificate' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - ReleaseNotes = '- xCertReq: + # ReleaseNotes of this module + ReleaseNotes = '- xCertReq: - Added additional parameters KeyLength, Exportable, ProviderName, OID, KeyUsage, CertificateTemplate, SubjectAltName - Fixed most markdown errors in Readme.md. - Corrected Parameter decoration format to be consistent with guidelines. ' - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' + } # End of PSData hashtable -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + } # End of PrivateData hashtable }