Skip to content

Commit

Permalink
Update DnsNameList for X509Certificate2 to use `X509SubjectAltern…
Browse files Browse the repository at this point in the history
…ativeNameExtension.EnumerateDnsNames` Method (PowerShell#24714)
  • Loading branch information
ArmaanMcleod authored Dec 29, 2024
1 parent 8cdb728 commit 10d1785
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 45 deletions.
71 changes: 26 additions & 45 deletions src/Microsoft.PowerShell.Security/security/CertificateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3309,82 +3309,63 @@ public EnhancedKeyUsageProperty(X509Certificate2 cert)
public sealed class DnsNameProperty
{
private readonly List<DnsNameRepresentation> _dnsList = new();
private readonly System.Globalization.IdnMapping idnMapping = new();
private readonly IdnMapping idnMapping = new();

private const string dnsNamePrefix = "DNS Name=";
private const string distinguishedNamePrefix = "CN=";

/// <summary>
/// Get property of DnsNameList.
/// </summary>
public List<DnsNameRepresentation> DnsNameList
public List<DnsNameRepresentation> DnsNameList => _dnsList;

private DnsNameRepresentation GetDnsNameRepresentation(string dnsName)
{
get
string unicodeName;

try
{
unicodeName = idnMapping.GetUnicode(dnsName);
}
catch (ArgumentException)
{
return _dnsList;
// The name is not valid Punycode, assume it's valid ASCII.
unicodeName = dnsName;
}

return new DnsNameRepresentation(dnsName, unicodeName);
}

/// <summary>
/// Constructor for DnsNameProperty.
/// </summary>
public DnsNameProperty(X509Certificate2 cert)
{
string name;
string unicodeName;
DnsNameRepresentation dnsName;
_dnsList = new List<DnsNameRepresentation>();

// extract DNS name from subject distinguish name
// if it exists and does not contain a comma
// a comma, indicates it is not a DNS name
if (cert.Subject.StartsWith(distinguishedNamePrefix, System.StringComparison.OrdinalIgnoreCase) &&
if (cert.Subject.StartsWith(distinguishedNamePrefix, StringComparison.OrdinalIgnoreCase) &&
!cert.Subject.Contains(','))
{
name = cert.Subject.Substring(distinguishedNamePrefix.Length);
try
{
unicodeName = idnMapping.GetUnicode(name);
}
catch (System.ArgumentException)
{
// The name is not valid punyCode, assume it's valid ascii.
unicodeName = name;
}

dnsName = new DnsNameRepresentation(name, unicodeName);
string parsedSubjectDistinguishedDnsName = cert.Subject.Substring(distinguishedNamePrefix.Length);
DnsNameRepresentation dnsName = GetDnsNameRepresentation(parsedSubjectDistinguishedDnsName);
_dnsList.Add(dnsName);
}

// Extract DNS names from SAN extensions
foreach (X509Extension extension in cert.Extensions)
{
// Filter to the OID for Subject Alternative Name
if (extension.Oid.Value == "2.5.29.17")
if (extension is X509SubjectAlternativeNameExtension sanExtension)
{
string[] names = extension.Format(true).Split(Environment.NewLine);
foreach (string nameLine in names)
foreach (string dnsNameEntry in sanExtension.EnumerateDnsNames())
{
// Get the part after 'DNS Name='
if (nameLine.StartsWith(dnsNamePrefix, System.StringComparison.InvariantCultureIgnoreCase))
{
name = nameLine.Substring(dnsNamePrefix.Length);
try
{
unicodeName = idnMapping.GetUnicode(name);
}
catch (System.ArgumentException)
{
// The name is not valid punyCode, assume it's valid ascii.
unicodeName = name;
}

dnsName = new DnsNameRepresentation(name, unicodeName);
DnsNameRepresentation dnsName = GetDnsNameRepresentation(dnsNameEntry);

// Only add the name if it is not the same as an existing name.
if (!_dnsList.Contains(dnsName))
{
_dnsList.Add(dnsName);
}
// Only add the name if it is not the same as an existing name.
if (!_dnsList.Contains(dnsName))
{
_dnsList.Add(dnsName);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,80 @@ Describe "Certificate Provider tests" -Tags "Feature" {
$certs.Thumbprint | Should -BeExactly $thumbprint
}
}

Context "SAN DNS Name Tests" {
BeforeAll {
$configFilePath = Join-Path -Path $TestDrive -ChildPath 'openssl.cnf'
$keyFilePath = Join-Path -Path $TestDrive -ChildPath 'privateKey.key'
$certFilePath = Join-Path -Path $TestDrive -ChildPath 'certificate.crt'
$pfxFilePath = Join-Path -Path $TestDrive -ChildPath 'certificate.pfx'
$password = New-CertificatePassword | ConvertFrom-SecureString -AsPlainText

$config = @"
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[ req_distinguished_name ]
CN = yourdomain.com
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = yourdomain.com
DNS.2 = www.yourdomain.com
DNS.3 = api.yourdomain.com
DNS.4 = xn--mnchen-3ya.com
DNS.5 = xn--80aaxitdbjr.com
DNS.6 = xn--caf-dma.com
"@

# Write the configuration to the specified path
Set-Content -Path $configFilePath -Value $config

# Generate the self-signed certificate with SANs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout $keyFilePath -out $certFilePath -config $configFilePath -extensions v3_req

# Create the PFX file
openssl pkcs12 -export -out $pfxFilePath -inkey $keyFilePath -in $certFilePath -passout pass:$password
}

It "Should set DNSNameList from SAN extensions" {
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxFilePath, $password)

$expectedDnsNameList = @(
[PSCustomObject]@{
Punycode = "yourdomain.com"
Unicode = "yourdomain.com"
}
[PSCustomObject]@{
Punycode = "www.yourdomain.com"
Unicode = "www.yourdomain.com"
}
[PSCustomObject]@{
Punycode = "api.yourdomain.com"
Unicode = "api.yourdomain.com"
}
[PSCustomObject]@{
Punycode = "xn--mnchen-3ya.com"
Unicode = "münchen.com"
}
[PSCustomObject]@{
Punycode = "xn--80aaxitdbjr.com"
Unicode = "папитрока.com"
}
[PSCustomObject]@{
Punycode = "xn--caf-dma.com"
Unicode = "café.com"
}
)

$cert | Should -Not -BeNullOrEmpty
$cert.DnsNameList | Should -HaveCount 6
($cert.DnsNameList | ConvertTo-Json -Compress) | Should -BeExactly ($expectedDnsNameList | ConvertTo-Json -Compress)
}
}
}

0 comments on commit 10d1785

Please sign in to comment.