From: Jeremy Barton Date: Tue, 19 Jun 2018 20:09:42 +0000 (-0700) Subject: Make the Linux TLS hostname comparison be case-insensitive X-Git-Tag: submit/tizen/20210909.063632~11031^2~4156 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=182c546a4ed4525d39292c01e4fdb214d42a0aea;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Make the Linux TLS hostname comparison be case-insensitive This change also adds direct tests to CheckX509Hostname, because we still haven't added a strong X509Extension subclass for Subject Alternative Name, which is where more direct exposure would live. Commit migrated from https://github.com/dotnet/corefx/commit/237ab14caf7e60f221e5d1d0103477f99c17509d --- diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/openssl.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native/openssl.c index dde8297..05c55fa 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/openssl.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/openssl.c @@ -736,7 +736,8 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname, { char c = candidateStr[i]; - if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '.') && (c != '-') && (c != '*' || i != 0)) + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '.') && (c != '-') && + (c != '*' || i != 0)) { return 0; } @@ -749,7 +750,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname, return 0; } - return !memcmp(candidateStr, hostname, (size_t)cchHostname); + return !strncasecmp((const char*)candidateStr, hostname, (size_t)cchHostname); } for (i = 0; i < cchHostname; ++i) @@ -782,7 +783,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname, return 0; } - return !memcmp(candidateStr + 1, hostname + hostnameFirstDot, (size_t)matchLength); + return !strncasecmp(candidateStr + 1, hostname + hostnameFirstDot, (size_t)matchLength); } } @@ -793,7 +794,7 @@ static int CheckX509HostnameMatch(ASN1_STRING* candidate, const char* hostname, return 0; } - return !memcmp(candidate->data, hostname, (size_t)cchHostname); + return !strncasecmp((const char*)candidate->data, hostname, (size_t)cchHostname); } /* diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/HostnameMatchTests.Unix.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/HostnameMatchTests.Unix.cs new file mode 100644 index 0000000..2a3b8b1 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/HostnameMatchTests.Unix.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public static class HostnameMatchTests + { + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public static void MatchCN_NoWildcards(bool wantsWildcard, bool mixedCase) + { + string targetName = "LocalHost.loCAldoMaIn"; + string subjectCN = wantsWildcard ? "*.LOcaLdomain" : targetName; + + RunTest(targetName, subjectCN, null, !mixedCase, !wantsWildcard); + } + + [Theory] + [InlineData("Capitalized.SomeDomain.TLD", false, true)] + [InlineData("Capitalized.SomeDomain.TLD", true, true)] + [InlineData("Too.Many.SomeDomain.TLD", false, false)] + [InlineData("Too.Many.SomeDomain.TLD", true, false)] + [InlineData("Now.Lower.SomeDomain.TLD", false, true)] + [InlineData("Now.Lower.SomeDomain.TLD", true, true)] + [InlineData("Score.1812-Overture.somedomain.TLD", false, true)] + [InlineData("Score.1812-Overture.somedomain.TLD", true, true)] + [InlineData("1-800.Lower.somedomain.TLD", false, true)] + [InlineData("1-800.Lower.somedomain.TLD", true, true)] + public static void MatchSubjectAltName(string targetName, bool mixedCase, bool expectedResult) + { + string[] sanEntries = + { + "Capitalized.SomeDomain.TLD", + "*.SomeDomain.TLD", + "*.lower.someDomain.Tld", + "*.1812-Overture.SomeDomain.Tld", + }; + + RunTest(targetName, "SAN Certificate", sanEntries, !mixedCase, expectedResult); + } + + [Fact] + public static void SubjectAltName_NoFallback() + { + string[] sanEntries = + { + "reference.example.org", + "other.example.org", + "reference.example", + }; + + RunTest("www.example.org", "www.example.org", sanEntries, false, false); + } + + private static void RunTest( + string targetName, + string subjectCN, + IList sanDnsNames, + bool flattenCase, + bool expectedResult) + { + using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams)) + { + CertificateRequest request = new CertificateRequest( + $"CN={FixCase(subjectCN, flattenCase)}, O=.NET Framework (CoreFX)", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + request.CertificateExtensions.Add( + new X509KeyUsageExtension( + X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.DigitalSignature, + false)); + + if (sanDnsNames != null) + { + var builder = new SubjectAlternativeNameBuilder(); + + foreach (string sanDnsName in sanDnsNames) + { + builder.AddDnsName(sanDnsName); + } + + X509Extension extension = builder.Build(); + + // The SAN builder will have done DNS case normalization via IdnMapping. + // We need to undo that here. + if (!flattenCase) + { + UTF8Encoding encoding = new UTF8Encoding(); + + byte[] extensionBytes = extension.RawData; + Span extensionSpan = extensionBytes; + + foreach (string sanDnsName in sanDnsNames) + { + // If the string is longer than 127 then the quick DER encoding check + // is not correct. + Assert.InRange(sanDnsName.Length, 1, 127); + + byte[] lowerBytes = encoding.GetBytes(sanDnsName.ToLowerInvariant()); + byte[] mixedBytes = encoding.GetBytes(sanDnsName); + + // Only 7-bit ASCII should be here, no byte expansion. + // (non-7-bit ASCII values require IdnMapping normalization) + Assert.Equal(sanDnsName.Length, lowerBytes.Length); + Assert.Equal(sanDnsName.Length, mixedBytes.Length); + + int idx = extensionSpan.IndexOf(lowerBytes); + + while (idx >= 0) + { + if (idx < 2 || + extensionBytes[idx - 2] != 0x82 || + extensionBytes[idx - 1] != sanDnsName.Length) + { + int relativeIdx = extensionSpan.Slice(idx + 1).IndexOf(lowerBytes); + idx = idx + 1 + relativeIdx; + continue; + } + + mixedBytes.AsSpan().CopyTo(extensionSpan.Slice(idx)); + break; + } + } + + extension.RawData = extensionBytes; + } + + request.CertificateExtensions.Add(extension); + } + + DateTimeOffset start = DateTimeOffset.UtcNow.AddYears(-1); + DateTimeOffset end = start.AddYears(1); + + using (X509Certificate2 cert = request.CreateSelfSigned(start, end)) + { + bool isMatch = CheckHostname(cert, targetName); + string lowerTarget = targetName.ToLowerInvariant(); + bool isLowerMatch = CheckHostname(cert, lowerTarget); + + if (expectedResult) + { + Assert.True(isMatch, $"{targetName} matches"); + Assert.True(isLowerMatch, $"{lowerTarget} (lowercase) matches"); + } + else + { + Assert.False(isMatch, $"{targetName} matches"); + Assert.False(isLowerMatch, $"{lowerTarget} (lowercase) matches"); + } + } + } + } + + private static string FixCase(string input, bool flatten) + { + return flatten ? input.ToLowerInvariant() : input; + } + + private static bool CheckHostname(X509Certificate2 cert, string targetName) + { + int value = CheckX509Hostname(cert.Handle, targetName, targetName.Length); + GC.KeepAlive(cert); + Assert.InRange(value, 0, 1); + return value != 0; + } + + [DllImport(Interop.Libraries.CryptoNative, EntryPoint = "CryptoNative_CheckX509Hostname")] + private static extern int CheckX509Hostname(IntPtr x509, string hostname, int cchHostname); + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index 985b99b..8202076 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -85,6 +85,7 @@ Common\System\IO\PersistedFiles.Names.Unix.cs +