Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSFT_xDSCWebService: Fixes issue with Certificates with Certificate Template Information #607

Closed
wants to merge 7 commits into from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Fixes issue matching a certificate in xDscWebService when Subject and
Template name is used and the matching certificate contains a Certificate
Template Information Oid.
[issue #606](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/606)
- Fixes style inconsistencies in PublishModulesAndMofsToPullServer.psm1.
[issue #530](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/530)
- Suppresses forced Verbose output in MSFT_xArchive.EndToEnd.Tests.ps1,
Expand Down
301 changes: 277 additions & 24 deletions DSCResources/MSFT_xDSCWebService/MSFT_xDSCWebService.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,13 @@ function Get-TargetResource
}
else
{
$certificate = ([System.Array] (Get-ChildItem -Path 'Cert:\LocalMachine\My\')) | Where-Object -FilterScript {
[System.Array] $certificate = Get-ChildItem -Path 'Cert:\LocalMachine\My\' | Where-Object -FilterScript {
$_.Thumbprint -eq $webBinding.CertificateHash
}

# Try to parse the Certificate Template Name. The property is not available on all Certificates.
$actualCertificateTemplateName = ''
$certificateTemplateProperty = $certificate.Extensions | Where-Object -FilterScript {
$_.Oid.FriendlyName -eq 'Certificate Template Name'
}

if ($null -ne $certificateTemplateProperty)
if ($certificate.Count -eq 1)
{
$actualCertificateTemplateName = $certificateTemplateProperty.Format($false)
$actualCertificateTemplateName = Get-CertificateTemplateName -Certificate $certificate
}

$output.Add('CertificateThumbPrint', $webBinding.CertificateHash)
Expand Down Expand Up @@ -1434,24 +1428,48 @@ function Find-CertificateThumbprintWithSubjectAndTemplateName

$filteredCertificates = @()

foreach ($oidFriendlyName in 'Certificate Template Name', 'Certificate Template Information')
{
# Only get certificates created from a template otherwise filtering by subject and template name will cause errors
[System.Array] $certificatesFromTemplates = (Get-ChildItem -Path $Store).Where{
$_.Extensions.Oid.FriendlyName -contains $oidFriendlyName
}
$certificatesWithSubject = (Get-ChildItem -Path $Store).Where({
$_.Subject -eq $Subject
})

$adTemplates = $null

switch ($oidFriendlyName)
foreach ($certificate in $certificatesWithSubject)
{
switch ($certificate.Extensions.Oid.FriendlyName)
{
'Certificate Template Name' {$templateMatchString = $TemplateName}
'Certificate Template Information' {$templateMatchString = '^Template={0}' -f $TemplateName}
}
'Certificate Template Information'
{
if ($null -eq $adTemplates)
{
$adTemplates = Get-CertificateTemplatesFromActiveDirectory
}

if ($null -ne $adTemplates)
{
$templateDisplayName = $adTemplates | Where-Object -FilterScript {
$_.Name -eq $TemplateName
} | Select-Object -ExpandProperty DisplayName

if ($null -ne $templateDisplayName -and
$certificate.Extensions.Where({
$_.Oid.FriendlyName -eq 'Certificate Template Information'
}).Format($false) -like ('Template={0}*' -f $templateDisplayName))
{
$filteredCertificates += $certificate
}
}
}

$filteredCertificates += $certificatesFromTemplates.Where{
$_.Subject -eq $Subject -and
$_.Extensions.Where{
$_.Oid.FriendlyName -eq $oidFriendlyName
}.Format($false) -match $templateMatchString
'Certificate Template Name'
{
if ($certificate.Extensions.Where({
$_.Oid.FriendlyName -eq 'Certificate Template Name'
}).Format($false) -eq $TemplateName)
{
$filteredCertificates += $certificate
}
}
}
}

Expand All @@ -1469,6 +1487,241 @@ function Find-CertificateThumbprintWithSubjectAndTemplateName
}
}

<#
.SYNOPSIS
Wraps a single ADSI command to get the domain naming context so it can be mocked.
#>
function Get-DomainNamingContext
{
[CmdletBinding()]
[OutputType([String])]
param ()

return ([System.DirectoryServices.DirectoryEntry] 'LDAP://RootDSE').Get('rootDomainNamingContext')
}

<#
.SYNOPSIS
Get certificate template names from Active Directory for x509 certificates.

.DESCRIPTION
Gets the certificate templates from Active Directory by using a
DirectorySearcher object to find all objects with an objectClass
of pKICertificateTemplate from the search root of
CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration

.NOTES
The domain variable is populated based on the domain of the user running the
function. When run as System this will return the domain of computer.
Normally this won't make any difference unless the user is from a foreign
domain.
#>
function Get-CertificateTemplatesFromActiveDirectory
{
[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param ()

try
{
$domain = Get-DomainNamingContext
$searcher = New-Object -TypeName DirectoryServices.DirectorySearcher

$searcher.Filter = '(objectclass=pKICertificateTemplate)'
$searcher.SearchRoot = 'LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,{0}' -f $domain

$searchResults = $searcher.FindAll()
}
catch
{
Write-Verbose -Message $_.Exception.Message
Write-Warning -Message $LocalizedData.ActiveDirectoryTemplateSearch
}

$adTemplates = @()

foreach ($searchResult in $searchResults)
{
$templateData = @{}
$properties = New-Object -TypeName Object[] -ArgumentList $searchResult.Properties.Count

$searchResult.Properties.CopyTo($properties, 0)
$properties.ForEach({
$templateData[$_.Name] = ($_.Value | Out-String).Trim()
})

$adTemplates += [PSCustomObject] $templateData
}

return $adTemplates
}

<#
.SYNOPSIS
Gets information about the certificate template.

.DESCRIPTION
This function returns the information about the certificate template by retreiving
the available templates from Active Directory and matching the formatted certificate
template name against this list.
In addition to the template name the display name, template OID, the major version
and minor version is also returned.

.PARAMETER FormattedTemplate
The text from the certificate template extension, retrieved from
Get-CertificateTemplateText.
#>
function Get-CertificateTemplateInformation
{
[OutputType([PSCustomObject])]
param
(
[Parameter(Mandatory = $true)]
[String]
$FormattedTemplate
)

$templateInformation = @{}

switch -Regex ($FormattedTemplate)
{
<#
Example of the certificate extension template text

Template=Display Name 1(1.3.6.1.4.1.311.21.8.5734392.6195358.14893705.12992936.3444946.62.3384218.1234567)
Major Version Number=100
Minor Version Number=5

If the Display Name of the template has not been found then FormattedText would like something like this.

Template=1.3.6.1.4.1.311.21.8.5734392.6195358.14893705.12992936.3444946.62.3384218.1234567
Major Version Number=100
Minor Version Number=5

The Name of the template is found by matching the OID or the Display Name against the list of temples in AD.
#>

'Template=(?:(?<DisplayName>.+)\((?<Oid>[\d.]+)\))|(?<Oid>[\d.]+)\s*Major\sVersion\sNumber=(?<MajorVersion>\d+)\s*Minor\sVersion\sNumber=(?<MinorVersion>\d+)'
{
[Array] $adTemplates = Get-CertificateTemplatesFromActiveDirectory

if ([String]::IsNullOrEmpty($Matches.DisplayName))
{
$template = $adTemplates.Where({
$_.'msPKI-Cert-Template-OID' -eq $Matches.Oid
})

$Matches['DisplayName'] = $template.DisplayName
}
else
{
$template = $adTemplates.Where({
$_.'DisplayName' -eq $Matches.DisplayName
})
}

$Matches['Name'] = $template.Name

if ($template.Count -eq 0)
{
Write-Warning -Message ($LocalizedData.TemplateNameResolutionError -f ('{0}({1})' -f $Matches.DisplayName, $Matches.Oid))
}

$templateInformation['Name'] = $Matches.Name
$templateInformation['DisplayName'] = $Matches.DisplayName
$templateInformation['Oid'] = $Matches.Oid
$templateInformation['MajorVersion'] = $Matches.MajorVersion
$templateInformation['MinorVersion'] = $Matches.MinorVersion
}

# The certificate extension template text just contains the name of the template so return that.

'^(?<TemplateName>\w+)\s?$'
{
$templateInformation['Name'] = $Matches.TemplateName
}

default
{
Write-Warning -Message ($LocalizedData.TemplateNameNotFound -f $FormattedTemplate)
}
}

return [PSCustomObject] $templateInformation
}

<#
.SYNOPSIS
Gets the formatted text output from an X509 certificate template extension.

.DESCRIPTION
The supplied X509 Extension Collected is processed to find any Certificate
Template extensions.
If a template extension is found the Format method is called with the parameter
$true and the text output is returned.

.PARAMETER TemplateExtensions
The X509 extensions collection from and X509 certificate to be searched for a
certificate template extension.
#>
function Get-CertificateTemplateExtensionText
{
[OutputType([String])]
param
(
[Parameter(Mandatory = $true)]
[System.Security.Cryptography.X509Certificates.X509ExtensionCollection]
$TemplateExtensions
)

$templateOidNames = @('Certificate Template Information', 'Certificate Template Name')

$templateExtension = $TemplateExtensions.Where({
$_.Oid.FriendlyName -in $templateOidNames
})[0]

if ($null -ne $templateExtension)
{
return $templateExtension.Format($true)
}
}

<#
.SYNOPSIS
Get a certificate template name from an x509 certificate.

.DESCRIPTION
Gets the name of the template used for the certificate that is passed to
this cmdlet by translating the OIDs "1.3.6.1.4.1.311.21.7" or
"1.3.6.1.4.1.311.20.2".

.PARAMETER Certificate
The certificate object the template name is needed for.
#>
function Get-CertificateTemplateName
{
[cmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter(Mandatory = $true)]
[object]
$Certificate
)

if ($Certificate -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2])
{
return $null
}

$templateExtensionText = Get-CertificateTemplateExtensionText -TemplateExtensions $Certificate.Extensions

if ($null -ne $templateExtensionText)
{
return Get-CertificateTemplateInformation -FormattedTemplate $templateExtensionText |
Select-Object -ExpandProperty Name
}
}
#endregion

Export-ModuleMember -Function *-TargetResource
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ ConvertFrom-StringData -StringData @'
FindCertificateBySubjectNotFound = Certificate not found with subject containing {0} and using template "{1}".
IISInstallationPathNotFound = IIS installation path not found
IISWebAdministrationAssemblyNotFound = IIS version of Microsoft.Web.Administration.dll not found
TemplateNameResolutionError = Failed to resolve the template name from Active Directory certificate templates [{0}].
TemplateNameNotFound = No template name found in Active Directory for [{0}].
ActiveDirectoryTemplateSearch = Failed to get the certificate templates from Active Directory.
'@
Loading