Skip to content

Commit 6c83e0d

Browse files
authored
Improve handling of encoding of X.520 attributes
1 parent 6ac8d05 commit 6c83e0d

File tree

3 files changed

+245
-33
lines changed
  • src/libraries

3 files changed

+245
-33
lines changed

src/libraries/Common/src/System/Security/Cryptography/Oids.cs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,52 @@ internal static partial class Oids
103103
// PKCS#7
104104
internal const string NoSignature = "1.3.6.1.5.5.7.6.2";
105105

106-
// X500 Names
107-
internal const string CommonName = "2.5.4.3";
108-
internal const string CountryOrRegionName = "2.5.4.6";
109-
internal const string LocalityName = "2.5.4.7";
110-
internal const string StateOrProvinceName = "2.5.4.8";
111-
internal const string Organization = "2.5.4.10";
112-
internal const string OrganizationalUnit = "2.5.4.11";
113-
internal const string EmailAddress = "1.2.840.113549.1.9.1";
106+
// X500 Names - T-REC X.520-201910
107+
internal const string KnowledgeInformation = "2.5.4.2"; // 6.1.1 - id-at-knowledgeInformation
108+
internal const string CommonName = "2.5.4.3"; // 6.2.2 - id-at-commonName
109+
internal const string Surname = "2.5.4.4"; // 6.2.3 - id-at-surname
110+
internal const string SerialNumber = "2.5.4.5"; // 6.2.9 - id-at-serialNumber
111+
internal const string CountryOrRegionName = "2.5.4.6"; // 6.3.1 - id-at-countryName
112+
internal const string LocalityName = "2.5.4.7"; // 6.3.4 - id-at-localityName
113+
internal const string StateOrProvinceName = "2.5.4.8"; // 6.3.5 - id-at-stateOrProvinceName
114+
internal const string StreetAddress = "2.5.4.9"; // 6.3.6 - id-at-streetAddress
115+
internal const string Organization = "2.5.4.10"; // 6.4.1 - id-at-organizationName
116+
internal const string OrganizationalUnit = "2.5.4.11"; // 6.4.2 - id-at-organizationalUnitName
117+
internal const string Title = "2.5.4.12"; // 6.4.3 - id-at-title
118+
internal const string Description = "2.5.4.13"; // 6.5.1 - id-at-description
119+
internal const string BusinessCategory = "2.5.4.15"; // 6.5.4 - id-at-businessCategory
120+
internal const string PostalCode = "2.5.4.17"; // 6.6.2 - id-at-postalCode
121+
internal const string PostOfficeBox = "2.5.4.18"; // 6.6.3 - id-at-postOfficeBox
122+
internal const string PhysicalDeliveryOfficeName = "2.5.4.19"; // 6.6.4 - id-at-physicalDeliveryOfficeName
123+
internal const string TelephoneNumber = "2.5.4.20"; // 6.7.1 - id-at-telephoneNumber
124+
internal const string X121Address = "2.5.4.24"; // 6.7.5 - id-at-x121Address
125+
internal const string InternationalISDNNumber = "2.5.4.25"; // 6.7.6 - id-at-internationalISDNNumber
126+
internal const string DestinationIndicator = "2.5.4.27"; // 6.7.8 - id-at-destinationIndicator
127+
internal const string Name = "2.5.4.41"; // 6.2.1 - id-at-name
128+
internal const string GivenName = "2.5.4.42"; // 6.2.4 - id-at-givenName
129+
internal const string Initials = "2.5.4.43"; // 6.2.5 - id-at-initials
130+
internal const string GenerationQualifier = "2.5.4.44"; // 6.2.6 - id-at-generationQualifier
131+
internal const string DnQualifier = "2.5.4.46"; // 6.2.8 - id-at-dnQualifier
132+
internal const string HouseIdentifier = "2.5.4.51"; // 6.3.7 - id-at-houseIdentifier
133+
internal const string DmdName = "2.5.4.54"; // 6.11.1 - id-at-dmdName
134+
internal const string Pseudonym = "2.5.4.65"; // 6.2.10 - id-at-pseudonym
135+
internal const string UiiInUrn = "2.5.4.80"; // 6.13.3 - id-at-uiiInUrn
136+
internal const string ContentUrl = "2.5.4.81"; // 6.13.4 - id-at-contentUrl
137+
internal const string Uri = "2.5.4.83"; // 6.2.12 - id-at-uri
138+
internal const string Urn = "2.5.4.86"; // 6.2.13 - id-at-urn
139+
internal const string Url = "2.5.4.87"; // 6.2.14 - id-at-url
140+
internal const string UrnC = "2.5.4.89"; // 6.12.4 - id-at-urnC
141+
internal const string EpcInUrn = "2.5.4.94"; // 6.13.9 - id-at-epcInUrn
142+
internal const string LdapUrl = "2.5.4.95"; // 6.13.10 - id-at-ldapUrl
143+
internal const string OrganizationIdentifier = "2.5.4.97"; // 6.4.4 - id-at-organizationIdentifier
144+
internal const string CountryOrRegionName3C = "2.5.4.98"; // 6.3.2 - id-at-countryCode3c
145+
internal const string CountryOrRegionName3N = "2.5.4.99"; // 6.3.3 - id-at-countryCode3n
146+
internal const string DnsName = "2.5.4.100"; // 6.2.15 - id-at-dnsName
147+
internal const string IntEmail = "2.5.4.104"; // 6.2.16 - id-at-intEmail
148+
internal const string JabberId = "2.5.4.105"; // 6.2.17 - id-at-jid
149+
150+
// RFC 2985
151+
internal const string EmailAddress = "1.2.840.113549.1.9.1"; // B.3.5
114152

115153
// Cert Extensions
116154
internal const string BasicConstraints = "2.5.29.10";

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500NameEncoder.cs

Lines changed: 126 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ namespace System.Security.Cryptography.X509Certificates
1111
{
1212
internal static partial class X500NameEncoder
1313
{
14+
private enum EncodingRules
15+
{
16+
Unknown,
17+
IA5String,
18+
DirectoryString,
19+
PrintableString,
20+
UTF8String,
21+
NumericString,
22+
}
23+
1424
private const string OidTagPrefix = "OID.";
1525
private const string UseSemicolonSeparators = ";";
1626
private const string UseCommaSeparators = ",";
@@ -20,6 +30,8 @@ internal static partial class X500NameEncoder
2030
private static readonly SearchValues<char> s_needsQuotingChars =
2131
SearchValues.Create(",+=\"\n<>#;"); // \r is NOT in this list, because it isn't in Windows.
2232

33+
private static readonly Lazy<Dictionary<string, EncodingRules>> s_lazyEncodingRulesLookup = new(CreateEncodingRulesLookup);
34+
2335
internal static string X500DistinguishedNameDecode(
2436
byte[] encodedName,
2537
bool printOid,
@@ -510,32 +522,35 @@ private static byte[] ParseRdn(ReadOnlySpan<char> tagOid, ReadOnlySpan<char> cha
510522
throw new CryptographicException(SR.Cryptography_Invalid_X500Name, e);
511523
}
512524

513-
if (tagOid.SequenceEqual(Oids.EmailAddress))
514-
{
515-
try
516-
{
517-
// An email address with an invalid value will throw.
518-
writer.WriteCharacterString(UniversalTagNumber.IA5String, data);
519-
}
520-
catch (EncoderFallbackException)
521-
{
522-
throw new CryptographicException(SR.Cryptography_Invalid_IA5String);
523-
}
524-
}
525-
else if (forceUtf8Encoding)
525+
switch (LookupEncodingRules(tagOid))
526526
{
527-
writer.WriteCharacterString(UniversalTagNumber.UTF8String, data);
528-
}
529-
else
530-
{
531-
try
532-
{
533-
writer.WriteCharacterString(UniversalTagNumber.PrintableString, data);
534-
}
535-
catch (EncoderFallbackException)
536-
{
537-
writer.WriteCharacterString(UniversalTagNumber.UTF8String, data);
538-
}
527+
case EncodingRules.IA5String:
528+
WriteCryptoCharacterString(writer, UniversalTagNumber.IA5String, data);
529+
break;
530+
case EncodingRules.UTF8String:
531+
case EncodingRules.DirectoryString or EncodingRules.Unknown when forceUtf8Encoding:
532+
WriteCryptoCharacterString(writer, UniversalTagNumber.UTF8String, data);
533+
break;
534+
case EncodingRules.NumericString:
535+
WriteCryptoCharacterString(writer, UniversalTagNumber.NumericString, data);
536+
break;
537+
case EncodingRules.PrintableString:
538+
WriteCryptoCharacterString(writer, UniversalTagNumber.PrintableString, data);
539+
break;
540+
case EncodingRules.DirectoryString:
541+
case EncodingRules.Unknown:
542+
try
543+
{
544+
writer.WriteCharacterString(UniversalTagNumber.PrintableString, data);
545+
}
546+
catch (EncoderFallbackException)
547+
{
548+
WriteCryptoCharacterString(writer, UniversalTagNumber.UTF8String, data);
549+
}
550+
break;
551+
default:
552+
Debug.Fail("Encoding rule was not handled.");
553+
goto case EncodingRules.Unknown;
539554
}
540555
}
541556

@@ -567,5 +582,91 @@ private static int ExtractValue(ReadOnlySpan<char> chars, Span<char> destination
567582

568583
return written;
569584
}
585+
586+
private static Dictionary<string, EncodingRules> CreateEncodingRulesLookup()
587+
{
588+
// Attributes that are not "obsolete" from ITU T-REC X.520-2019.
589+
// Attributes that are included are attributes that are string-like and can be represented by a String.
590+
// Windows does not have any restrictions on encoding non-string encodable types, it will encode them
591+
// anyway, such as OID.2.5.4.14=test will encode test as a PrintableString, even though the OID is a SET.
592+
// To maintain similar behavior as Windows, those types will remain treated as unknown.
593+
const int LookupDictionarySize = 43;
594+
Dictionary<string, EncodingRules> lookup = new(LookupDictionarySize, StringComparer.Ordinal)
595+
{
596+
{ Oids.KnowledgeInformation, EncodingRules.DirectoryString },
597+
{ Oids.CommonName, EncodingRules.DirectoryString },
598+
{ Oids.Surname, EncodingRules.DirectoryString },
599+
{ Oids.SerialNumber, EncodingRules.PrintableString },
600+
{ Oids.CountryOrRegionName, EncodingRules.PrintableString },
601+
{ Oids.LocalityName, EncodingRules.DirectoryString },
602+
{ Oids.StateOrProvinceName, EncodingRules.DirectoryString },
603+
{ Oids.StreetAddress, EncodingRules.DirectoryString },
604+
{ Oids.Organization, EncodingRules.DirectoryString },
605+
{ Oids.OrganizationalUnit, EncodingRules.DirectoryString },
606+
{ Oids.Title, EncodingRules.DirectoryString },
607+
{ Oids.Description, EncodingRules.DirectoryString },
608+
{ Oids.BusinessCategory, EncodingRules.DirectoryString },
609+
{ Oids.PostalCode, EncodingRules.DirectoryString },
610+
{ Oids.PostOfficeBox, EncodingRules.DirectoryString },
611+
{ Oids.PhysicalDeliveryOfficeName, EncodingRules.DirectoryString },
612+
{ Oids.TelephoneNumber, EncodingRules.PrintableString },
613+
{ Oids.X121Address, EncodingRules.NumericString },
614+
{ Oids.InternationalISDNNumber, EncodingRules.NumericString },
615+
{ Oids.DestinationIndicator, EncodingRules.PrintableString },
616+
{ Oids.Name, EncodingRules.DirectoryString },
617+
{ Oids.GivenName, EncodingRules.DirectoryString },
618+
{ Oids.Initials, EncodingRules.DirectoryString },
619+
{ Oids.GenerationQualifier, EncodingRules.DirectoryString },
620+
{ Oids.DnQualifier, EncodingRules.PrintableString },
621+
{ Oids.HouseIdentifier, EncodingRules.DirectoryString },
622+
{ Oids.DmdName, EncodingRules.DirectoryString },
623+
{ Oids.Pseudonym, EncodingRules.DirectoryString },
624+
{ Oids.UiiInUrn, EncodingRules.UTF8String },
625+
{ Oids.ContentUrl, EncodingRules.UTF8String },
626+
{ Oids.Uri, EncodingRules.UTF8String },
627+
{ Oids.Urn, EncodingRules.UTF8String },
628+
{ Oids.Url, EncodingRules.UTF8String },
629+
{ Oids.UrnC, EncodingRules.PrintableString },
630+
{ Oids.EpcInUrn, EncodingRules.DirectoryString },
631+
{ Oids.LdapUrl, EncodingRules.UTF8String },
632+
{ Oids.OrganizationIdentifier, EncodingRules.DirectoryString },
633+
{ Oids.CountryOrRegionName3C, EncodingRules.PrintableString },
634+
{ Oids.CountryOrRegionName3N, EncodingRules.NumericString },
635+
{ Oids.DnsName, EncodingRules.UTF8String },
636+
{ Oids.IntEmail, EncodingRules.UTF8String },
637+
{ Oids.JabberId, EncodingRules.UTF8String },
638+
{ Oids.EmailAddress, EncodingRules.IA5String },
639+
};
640+
641+
Debug.Assert(lookup.Count == LookupDictionarySize);
642+
return lookup;
643+
}
644+
645+
private static void WriteCryptoCharacterString(AsnWriter writer, UniversalTagNumber tagNumber, ReadOnlySpan<char> data)
646+
{
647+
try
648+
{
649+
writer.WriteCharacterString(tagNumber, data);
650+
}
651+
catch (EncoderFallbackException)
652+
{
653+
if (tagNumber == UniversalTagNumber.IA5String)
654+
{
655+
throw new CryptographicException(SR.Cryptography_Invalid_IA5String);
656+
}
657+
else
658+
{
659+
throw new CryptographicException(SR.Cryptography_Invalid_X500Name);
660+
}
661+
}
662+
}
663+
664+
private static EncodingRules LookupEncodingRules(ReadOnlySpan<char> oid)
665+
{
666+
Dictionary<string, EncodingRules> lookup = s_lazyEncodingRulesLookup.Value;
667+
Dictionary<string, EncodingRules>.AlternateLookup<ReadOnlySpan<char>> alternateLookup =
668+
lookup.GetAlternateLookup<ReadOnlySpan<char>>();
669+
return alternateLookup.TryGetValue(oid, out EncodingRules rules) ? rules : EncodingRules.Unknown;
670+
}
570671
}
571672
}

src/libraries/System.Security.Cryptography/tests/X509Certificates/NameTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Formats.Asn1;
45
using Test.Cryptography;
56
using Xunit;
67

@@ -127,5 +128,77 @@ public static void TestFormat(bool multiLine)
127128

128129
Assert.Equal(expected, formatted);
129130
}
131+
132+
[Theory]
133+
[InlineData("G=DotNet", UniversalTagNumber.UTF8String)]
134+
[InlineData("L=Alexandria", UniversalTagNumber.UTF8String)]
135+
[InlineData("O=GitHub", UniversalTagNumber.UTF8String)]
136+
[InlineData("OU=ProdSec", UniversalTagNumber.UTF8String)]
137+
[InlineData("S=Virginia", UniversalTagNumber.UTF8String)]
138+
[InlineData("SN=Doe", UniversalTagNumber.UTF8String)]
139+
[InlineData("ST=Main", UniversalTagNumber.UTF8String)]
140+
[InlineData("T=Pancake", UniversalTagNumber.UTF8String)]
141+
[InlineData("CN=Foo", UniversalTagNumber.UTF8String)]
142+
[InlineData("I=DD", UniversalTagNumber.UTF8String)]
143+
[InlineData("E=noone@example.com", UniversalTagNumber.IA5String)]
144+
[InlineData("OID.2.5.4.11=ProdSec", UniversalTagNumber.UTF8String)]
145+
[InlineData("OID.2.5.4.43=DD", UniversalTagNumber.UTF8String)]
146+
[InlineData("OID.2.25.77135202736018529853602245419149860647=sample", UniversalTagNumber.UTF8String)]
147+
[InlineData("C=US", UniversalTagNumber.PrintableString)]
148+
[InlineData("OID.2.5.4.20=\"+0 (555) 555-1234\"", UniversalTagNumber.PrintableString)]
149+
[InlineData("OID.2.5.4.99=840", UniversalTagNumber.NumericString, true)]
150+
[InlineData("OID.2.5.4.98=USA", UniversalTagNumber.PrintableString, true)]
151+
[InlineData("SERIALNUMBER=1234ABC", UniversalTagNumber.PrintableString)]
152+
public static void Encode_ForceUtf8EncodingForEligibleComponents(
153+
string distinguishedName,
154+
UniversalTagNumber tagNumber,
155+
bool nonWindowsOnly = false)
156+
{
157+
if (PlatformDetection.IsWindows && nonWindowsOnly)
158+
{
159+
return;
160+
}
161+
162+
X500DistinguishedName name = new(distinguishedName, X500DistinguishedNameFlags.ForceUTF8Encoding);
163+
byte[] encoded = name.RawData;
164+
165+
AsnValueReader reader = new(encoded, AsnEncodingRules.DER);
166+
AsnValueReader component = reader.ReadSequence();
167+
reader.ThrowIfNotEmpty();
168+
AsnValueReader rdn = component.ReadSetOf();
169+
component.ThrowIfNotEmpty();
170+
AsnValueReader value = rdn.ReadSequence();
171+
rdn.ThrowIfNotEmpty();
172+
173+
value.ReadObjectIdentifier();
174+
Assert.Equal(new Asn1Tag(tagNumber), value.PeekTag());
175+
}
176+
177+
[Theory]
178+
[InlineData("C=$$")]
179+
[InlineData("C=\"$$\"")]
180+
[InlineData("E=\uD83C\uDF4C")] // banana
181+
[InlineData("OID.2.5.4.99=a", true)]
182+
[InlineData("OID.2.5.4.6=$$")]
183+
public static void Encode_InvalidCharactersThrowCryptographicException(
184+
string distinguishedName,
185+
bool nonWindowsOnly = false)
186+
{
187+
if (PlatformDetection.IsWindows && nonWindowsOnly)
188+
{
189+
return;
190+
}
191+
192+
Assert.ThrowsAny<CryptographicException>(() =>
193+
new X500DistinguishedName(distinguishedName, X500DistinguishedNameFlags.ForceUTF8Encoding));
194+
}
195+
196+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows))]
197+
[InlineData(X500DistinguishedNameFlags.None)]
198+
[InlineData(X500DistinguishedNameFlags.ForceUTF8Encoding)]
199+
public static void Encode_FailsForIncorrectSurrogatePair(X500DistinguishedNameFlags flags)
200+
{
201+
Assert.ThrowsAny<CryptographicException>(() => new X500DistinguishedName("CN=\uD800", flags));
202+
}
130203
}
131204
}

0 commit comments

Comments
 (0)