Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit d1dafb3

Browse files
bartonjsstephentoub
authored andcommitted
Make the Linux TLS hostname comparison be case-insensitive (#30553)
This change also adds direct tests to CheckX509Hostname.
1 parent ee567f3 commit d1dafb3

File tree

3 files changed

+187
-4
lines changed

3 files changed

+187
-4
lines changed

src/Native/Unix/System.Security.Cryptography.Native/openssl.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,8 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname,
736736
{
737737
char c = candidateStr[i];
738738

739-
if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '.') && (c != '-') && (c != '*' || i != 0))
739+
if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '.') && (c != '-') &&
740+
(c != '*' || i != 0))
740741
{
741742
return 0;
742743
}
@@ -749,7 +750,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname,
749750
return 0;
750751
}
751752

752-
return !memcmp(candidateStr, hostname, static_cast<size_t>(cchHostname));
753+
return !strncasecmp(candidateStr, hostname, static_cast<size_t>(cchHostname));
753754
}
754755

755756
for (i = 0; i < cchHostname; ++i)
@@ -782,7 +783,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname,
782783
return 0;
783784
}
784785

785-
return !memcmp(candidateStr + 1, hostname + hostnameFirstDot, static_cast<size_t>(matchLength));
786+
return !strncasecmp(candidateStr + 1, hostname + hostnameFirstDot, static_cast<size_t>(matchLength));
786787
}
787788
}
788789

@@ -793,7 +794,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname,
793794
return 0;
794795
}
795796

796-
return !memcmp(candidate->data, hostname, static_cast<size_t>(cchHostname));
797+
return !strncasecmp(reinterpret_cast<char*>(candidate->data), hostname, static_cast<size_t>(cchHostname));
797798
}
798799

799800
/*
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using Xunit;
9+
10+
namespace System.Security.Cryptography.X509Certificates.Tests
11+
{
12+
public static class HostnameMatchTests
13+
{
14+
[Theory]
15+
[InlineData(false, false)]
16+
[InlineData(false, true)]
17+
[InlineData(true, false)]
18+
[InlineData(true, true)]
19+
public static void MatchCN_NoWildcards(bool wantsWildcard, bool mixedCase)
20+
{
21+
string targetName = "LocalHost.loCAldoMaIn";
22+
string subjectCN = wantsWildcard ? "*.LOcaLdomain" : targetName;
23+
24+
RunTest(targetName, subjectCN, null, !mixedCase, !wantsWildcard);
25+
}
26+
27+
[Theory]
28+
[InlineData("Capitalized.SomeDomain.TLD", false, true)]
29+
[InlineData("Capitalized.SomeDomain.TLD", true, true)]
30+
[InlineData("Too.Many.SomeDomain.TLD", false, false)]
31+
[InlineData("Too.Many.SomeDomain.TLD", true, false)]
32+
[InlineData("Now.Lower.SomeDomain.TLD", false, true)]
33+
[InlineData("Now.Lower.SomeDomain.TLD", true, true)]
34+
[InlineData("Score.1812-Overture.somedomain.TLD", false, true)]
35+
[InlineData("Score.1812-Overture.somedomain.TLD", true, true)]
36+
[InlineData("1-800.Lower.somedomain.TLD", false, true)]
37+
[InlineData("1-800.Lower.somedomain.TLD", true, true)]
38+
public static void MatchSubjectAltName(string targetName, bool mixedCase, bool expectedResult)
39+
{
40+
string[] sanEntries =
41+
{
42+
"Capitalized.SomeDomain.TLD",
43+
"*.SomeDomain.TLD",
44+
"*.lower.someDomain.Tld",
45+
"*.1812-Overture.SomeDomain.Tld",
46+
};
47+
48+
RunTest(targetName, "SAN Certificate", sanEntries, !mixedCase, expectedResult);
49+
}
50+
51+
[Fact]
52+
public static void SubjectAltName_NoFallback()
53+
{
54+
string[] sanEntries =
55+
{
56+
"reference.example.org",
57+
"other.example.org",
58+
"reference.example",
59+
};
60+
61+
RunTest("www.example.org", "www.example.org", sanEntries, false, false);
62+
}
63+
64+
private static void RunTest(
65+
string targetName,
66+
string subjectCN,
67+
IList<string> sanDnsNames,
68+
bool flattenCase,
69+
bool expectedResult)
70+
{
71+
using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams))
72+
{
73+
CertificateRequest request = new CertificateRequest(
74+
$"CN={FixCase(subjectCN, flattenCase)}, O=.NET Framework (CoreFX)",
75+
rsa,
76+
HashAlgorithmName.SHA256,
77+
RSASignaturePadding.Pkcs1);
78+
79+
request.CertificateExtensions.Add(
80+
new X509KeyUsageExtension(
81+
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.DigitalSignature,
82+
false));
83+
84+
if (sanDnsNames != null)
85+
{
86+
var builder = new SubjectAlternativeNameBuilder();
87+
88+
foreach (string sanDnsName in sanDnsNames)
89+
{
90+
builder.AddDnsName(sanDnsName);
91+
}
92+
93+
X509Extension extension = builder.Build();
94+
95+
// The SAN builder will have done DNS case normalization via IdnMapping.
96+
// We need to undo that here.
97+
if (!flattenCase)
98+
{
99+
UTF8Encoding encoding = new UTF8Encoding();
100+
101+
byte[] extensionBytes = extension.RawData;
102+
Span<byte> extensionSpan = extensionBytes;
103+
104+
foreach (string sanDnsName in sanDnsNames)
105+
{
106+
// If the string is longer than 127 then the quick DER encoding check
107+
// is not correct.
108+
Assert.InRange(sanDnsName.Length, 1, 127);
109+
110+
byte[] lowerBytes = encoding.GetBytes(sanDnsName.ToLowerInvariant());
111+
byte[] mixedBytes = encoding.GetBytes(sanDnsName);
112+
113+
// Only 7-bit ASCII should be here, no byte expansion.
114+
// (non-7-bit ASCII values require IdnMapping normalization)
115+
Assert.Equal(sanDnsName.Length, lowerBytes.Length);
116+
Assert.Equal(sanDnsName.Length, mixedBytes.Length);
117+
118+
int idx = extensionSpan.IndexOf(lowerBytes);
119+
120+
while (idx >= 0)
121+
{
122+
if (idx < 2 ||
123+
extensionBytes[idx - 2] != 0x82 ||
124+
extensionBytes[idx - 1] != sanDnsName.Length)
125+
{
126+
int relativeIdx = extensionSpan.Slice(idx + 1).IndexOf(lowerBytes);
127+
idx = idx + 1 + relativeIdx;
128+
continue;
129+
}
130+
131+
mixedBytes.AsSpan().CopyTo(extensionSpan.Slice(idx));
132+
break;
133+
}
134+
}
135+
136+
extension.RawData = extensionBytes;
137+
}
138+
139+
request.CertificateExtensions.Add(extension);
140+
}
141+
142+
DateTimeOffset start = DateTimeOffset.UtcNow.AddYears(-1);
143+
DateTimeOffset end = start.AddYears(1);
144+
145+
using (X509Certificate2 cert = request.CreateSelfSigned(start, end))
146+
{
147+
bool isMatch = CheckHostname(cert, targetName);
148+
string lowerTarget = targetName.ToLowerInvariant();
149+
bool isLowerMatch = CheckHostname(cert, lowerTarget);
150+
151+
if (expectedResult)
152+
{
153+
Assert.True(isMatch, $"{targetName} matches");
154+
Assert.True(isLowerMatch, $"{lowerTarget} (lowercase) matches");
155+
}
156+
else
157+
{
158+
Assert.False(isMatch, $"{targetName} matches");
159+
Assert.False(isLowerMatch, $"{lowerTarget} (lowercase) matches");
160+
}
161+
}
162+
}
163+
}
164+
165+
private static string FixCase(string input, bool flatten)
166+
{
167+
return flatten ? input.ToLowerInvariant() : input;
168+
}
169+
170+
private static bool CheckHostname(X509Certificate2 cert, string targetName)
171+
{
172+
int value = CheckX509Hostname(cert.Handle, targetName, targetName.Length);
173+
GC.KeepAlive(cert);
174+
Assert.InRange(value, 0, 1);
175+
return value != 0;
176+
}
177+
178+
[DllImport(Interop.Libraries.CryptoNative, EntryPoint = "CryptoNative_CheckX509Hostname")]
179+
private static extern int CheckX509Hostname(IntPtr x509, string hostname, int cchHostname);
180+
}
181+
}

src/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
<Compile Include="$(CommonPath)\System\IO\PersistedFiles.Names.Unix.cs">
8989
<Link>Common\System\IO\PersistedFiles.Names.Unix.cs</Link>
9090
</Compile>
91+
<Compile Include="HostnameMatchTests.Unix.cs" />
9192
<Compile Include="TestEnvironmentConfiguration.Unix.cs" />
9293
</ItemGroup>
9394
<ItemGroup>

0 commit comments

Comments
 (0)