DSCResources/cLCMCertUpdate/cLCMCertUpdate.psm1
###################################################################################### # The Get-TargetResource cmdlet. # This function will get the certificate if it exists and return all information ###################################################################################### function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)][System.String]$OutPath, [parameter(Mandatory = $false)][ValidateSet("Computer","FQDN","GUID")][System.String]$OutputName = "Computer", [parameter(Mandatory = $false)][PSCredential]$OutPathCredential, [parameter(Mandatory = $false)][System.String]$TemplateName, [parameter(Mandatory = $false)][System.String]$CertName, [parameter(Mandatory = $false)][System.String]$SubjectAlternativeName, [parameter(Mandatory = $false)][DateTime]$ExpireAfter ) ValidateProperties @PSBoundParameters -Report } ###################################################################################### # The Set-TargetResource cmdlet. # This function will pass the "apply" switch back to the validate function ###################################################################################### function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)][System.String]$OutPath, [parameter(Mandatory = $false)][ValidateSet("Computer","FQDN","GUID")][System.String]$OutputName = "Computer", [parameter(Mandatory = $false)][PSCredential]$OutPathCredential, [parameter(Mandatory = $false)][System.String]$TemplateName, [parameter(Mandatory = $false)][System.String]$CertName, [parameter(Mandatory = $false)][System.String]$SubjectAlternativeName, [parameter(Mandatory = $false)][DateTime]$ExpireAfter ) ValidateProperties @PSBoundParameters -Apply } ###################################################################################### # The Test-TargetResource cmdlet. # This function will only return a $true $false on compliance ###################################################################################### function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)][System.String]$OutPath, [parameter(Mandatory = $false)][ValidateSet("Computer","FQDN","GUID")][System.String]$OutputName = "Computer", [parameter(Mandatory = $false)][PSCredential]$OutPathCredential, [parameter(Mandatory = $false)][System.String]$TemplateName, [parameter(Mandatory = $false)][System.String]$CertName, [parameter(Mandatory = $false)][System.String]$SubjectAlternativeName, [parameter(Mandatory = $false)][DateTime]$ExpireAfter ) ValidateProperties @PSBoundParameters } ###################################################################################### # The ValidateProperties cmdlet. # This function accepts an -apply flag and "does the work" ###################################################################################### function ValidateProperties { param ( [parameter(Mandatory = $true)][System.String]$OutPath, [parameter(Mandatory = $false)][ValidateSet("Computer","FQDN","GUID")][System.String]$OutputName = "Computer", [parameter(Mandatory = $false)][PSCredential]$OutPathCredential, [parameter(Mandatory = $false)][System.String]$TemplateName, [parameter(Mandatory = $false)][System.String]$CertName, [parameter(Mandatory = $false)][System.String]$SubjectAlternativeName, [parameter(Mandatory = $false)][DateTime]$ExpireAfter, [Switch]$Apply, [Switch]$Report ) #Set initial TestedOK value to true, which will be called later to see if all variables are still valid [boolean]$TestedOK = $true [boolean]$CertNeedsUpdate = $false #Gather currently configured certificate information [string]$ActiveThumbprint = (Get-DscLocalConfigurationManager).CertificateID $ActiveCert = Get-Childitem cert:\LocalMachine\MY | where-object { $_.thumbprint -eq $ActiveThumbprint} IF ($ActiveCert) { $ActiveTemplate = ($ActiveCert.extensions | where-object{$_.oid.Friendlyname -match "Certificate Template Information"}).format(0).split(",")[0] $ActiveTemplate = $ActiveTemplate.trimstart("Template=").split("(")[0] $ActiveSANInfo = ($ActiveCert.extensions | where-object{$_.oid.Friendlyname -match "subject alternative name"}).Format(0).split(",") $ActiveSANInfo = $ActiveSANinfo.trimstart() write-verbose "Found Active Certificate $ActiveCertThumbprint" } Else { write-verbose "No Active Certificate has been found." } #Gather "Best Match" Certificate by first grabbing all valid certificate candidates by checking date and private key [datetime]$currdate = Get-Date $BestMatchCert = Get-Childitem cert:\LocalMachine\MY | Where-Object {($_.HasPrivateKey -eq $true) -and ($_.NotBefore -lt $currdate) -and $_.NotAfter -gt $currdate} #Use parameter-defined criteria to filter down the list, then pick the longest valid time from remaining choices If ($CertName) { $BestMatchCert = $BestMatchCert | Where-Object {$_.Subject -eq $CertName} } If ($TemplateName) { $gather = @() $tempname = $null $BestMatchCert = ($BestMatchCert | Where-Object {($_.extensions.oid.FriendlyName -match "Certificate Template Information")}) ForEach ($cert in $BestMatchCert) { $tempname = ($cert.extensions | where-object{$_.oid.Friendlyname -match "Certificate Template Information"}).format(0).split(",")[0] $tempname = $tempname.trimstart("Template=").split("(")[0] If($tempname -eq $templateName) {$gather+=$Cert} }#End ForEach Loop $BestMatchCert = $gather } If ($SubjectAlternativeName) { $gather = @() $tempname = $null $BestMatchCert = ($BestMatchCert | Where-Object {($_.extensions.oid.FriendlyName -match "subject alternative name")}) ForEach ($cert in $BestMatchCert) { $tempname = ($cert.extensions | where-object{$_.oid.Friendlyname -match "subject alternative name"}).Format(0).split(",") $tempname = $tempname.trimstart() If($tempname -contains $SubjectAlternativeName) {$gather+=$Cert} }#End ForEach Loop $BestMatchCert = $gather } If ($ExpireAfter) { $BestMatchCert = $BestMatchCert | Where-Object {$_.NotAfter -gt $ExpireAfter} } #trim and select final cert $BestMatchCert = $BestMatchCert | Sort-Object NotAfter -Descending | Select -First 1 If ($BestMatchCert) { $BestMatchTemplate = ($BestMatchCert.extensions | where-object{$_.oid.Friendlyname -match "Certificate Template Information"}).format(0).split(",")[0] $BestMatchTemplate = $BestMatchTemplate.trimstart("Template=").split("(")[0] $BestMatchSANInfo = ($BestMatchCert.extensions | where-object{$_.oid.Friendlyname -match "subject alternative name"}).Format(0).split(",") $BestMatchSANInfo = $BestMatchSANinfo.trimstart() $BestMatchCertThumbprint = $BestMatchCert.thumbprint write-verbose "Found Best Matching Certificate $BestMatchCertThumbprint" } else { write-verbose "No certificate could be found that met the criteria" } #Compare Active and BestMatch certs to determine update status for reports If ($ActiveCert.thumbprint -eq $BestMatchCert.thumbprint) {$UpdateStatus = "Current"} Else {$UpdateStatus = "Outdated"} #Compare ActiveCert to BestMatchCert and then update share if they differ If (($ActiveCert.thumbprint -ne $BestMatchCert.thumbprint) -and !$Report) { Write-Verbose "Certificates dont match, checking $outpath" #Generate Login Credentials if needed If ($OutPathCredential) { ($oldToken, $context, $newToken) = ImpersonateAs -cred $OutPathCredential } #Grab appropriate filename based on switch Switch ($OutputName) { Computer {$FileName = $env:COMPUTERNAME} FQDN {$FileName = $env:COMPUTERNAME+"."+(Get-WmiObject win32_computersystem).Domain} GUID {$FileName = (Get-DscLocalConfigurationManager).ConfigurationID} } #Check for the existence of the target path If(Test-Path -Path ($OutPath+'\'+$FileName+'.cer')) { write-verbose "cert found, comparing thumbprints" # X509Certificate2 object that will represent the certificate $CertPrint = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 # Imports the certificate from file to x509Certificate object $certPrint.Import($OutPath+'\'+$FileName+'.cer') If ($CertPrint.Thumbprint -eq $BestMatchCert.Thumbprint) { write-verbose "the file is already the correct version" } #Set Flag to Update if Thumbprints dont match If(($CertPrint.Thumbprint -ne $BestMatchCert.Thumbprint) -and $Apply) { write-verbose "Best match and cert in outpath don't match" $CertNeedsUpdate = $true } If(($CertPrint.Thumbprint -ne $BestMatchCert.Thumbprint) -and !$Apply) { write-verbose "Best match and cert in outpath don't match" $TestedOK = $false } }#End File Found #Can't find a file, next steps Else { write-verbose "file not found in outpath" $TestedOK = $false #Test writing to the certstore, then commit to adding a file Try { Write-Verbose "test writing to $OutPath ..." new-item "$OutPath\$FileName.txt" -ItemType File | Out-Null -ErrorAction Stop remove-item "$OutPath\$FileName.txt" -ErrorAction Stop write-verbose "... Success. OK to update" $CertNeedsUpdate = $true } Catch { Write-Verbose "...Failed. Can't write or change files in $OutPath." Throw $_ } } #Update The Certificate if Flagged IF(($CertNeedsUpdate -eq $true) -and $BestMatchCert -and $Apply) { Try { write-verbose "Attempting to update/add $OutPath\$FileName.cer" $DestinationFile = $OutPath+'\'+$FileName+'.cer' Export-Certificate -FilePath $DestinationFile -Cert $BestMatchCert -Force | Out-Null -ErrorAction Stop $TestedOK = $true } Catch { $TestedOK = $false Throw "Cannot write $DestinationFile" } }#End CertUpdate } #Logout if logged in if ($context) { $context.Undo() $context.Dispose() CloseUserToken($newToken) } #Return the appropriate data depending on Report/Apply flags (set returns no data) If ($Report) { $ReturnValue = @{ Thumbprint = $ActiveCert.Thumbprint CertName = $ActiveCert.Subject TemplateName = $ActiveTemplate SubjectAlternativeName = $SANInfo ExpireAfter = $ActiveCert.NotAfter UpdateStatus = $UpdateStatus } Return $ReturnValue } ElseIf (!($Apply)) { Return $TestedOK } } ###################################################################################### # The below functions are used for user impersonation # There are 3 functions in total ###################################################################################### function Get-ImpersonatetLib { if ($script:ImpersonateLib) { return $script:ImpersonateLib } $sig = @' [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken); [DllImport("kernel32.dll")] public static extern Boolean CloseHandle(IntPtr hObject); '@ $script:ImpersonateLib = Add-Type -PassThru -Namespace 'Lib.Impersonation' -Name ImpersonationLib -MemberDefinition $sig return $script:ImpersonateLib } function ImpersonateAs([PSCredential] $cred) { [IntPtr] $userToken = [Security.Principal.WindowsIdentity]::GetCurrent().Token $userToken $ImpersonateLib = Get-ImpersonatetLib $bLogin = $ImpersonateLib::LogonUser($cred.GetNetworkCredential().UserName, $cred.GetNetworkCredential().Domain, $cred.GetNetworkCredential().Password, 9, 0, [ref]$userToken) if ($bLogin) { $Identity = New-Object Security.Principal.WindowsIdentity $userToken $context = $Identity.Impersonate() } else { throw "Can't Logon as User $cred.GetNetworkCredential().UserName." } $context, $userToken } function CloseUserToken([IntPtr] $token) { $ImpersonateLib = Get-ImpersonatetLib $bLogin = $ImpersonateLib::CloseHandle($token) if (!$bLogin) { throw "Can't close token" } } # FUNCTIONS TO BE EXPORTED Export-ModuleMember -Function *-TargetResource |