Skip to content

Commit

Permalink
Improve handling of encoding of X.520 attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
vcsjones authored Nov 1, 2024
1 parent 6ac8d05 commit 6c83e0d
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 33 deletions.
54 changes: 46 additions & 8 deletions src/libraries/Common/src/System/Security/Cryptography/Oids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,52 @@ internal static partial class Oids
// PKCS#7
internal const string NoSignature = "1.3.6.1.5.5.7.6.2";

// X500 Names
internal const string CommonName = "2.5.4.3";
internal const string CountryOrRegionName = "2.5.4.6";
internal const string LocalityName = "2.5.4.7";
internal const string StateOrProvinceName = "2.5.4.8";
internal const string Organization = "2.5.4.10";
internal const string OrganizationalUnit = "2.5.4.11";
internal const string EmailAddress = "1.2.840.113549.1.9.1";
// X500 Names - T-REC X.520-201910
internal const string KnowledgeInformation = "2.5.4.2"; // 6.1.1 - id-at-knowledgeInformation
internal const string CommonName = "2.5.4.3"; // 6.2.2 - id-at-commonName
internal const string Surname = "2.5.4.4"; // 6.2.3 - id-at-surname
internal const string SerialNumber = "2.5.4.5"; // 6.2.9 - id-at-serialNumber
internal const string CountryOrRegionName = "2.5.4.6"; // 6.3.1 - id-at-countryName
internal const string LocalityName = "2.5.4.7"; // 6.3.4 - id-at-localityName
internal const string StateOrProvinceName = "2.5.4.8"; // 6.3.5 - id-at-stateOrProvinceName
internal const string StreetAddress = "2.5.4.9"; // 6.3.6 - id-at-streetAddress
internal const string Organization = "2.5.4.10"; // 6.4.1 - id-at-organizationName
internal const string OrganizationalUnit = "2.5.4.11"; // 6.4.2 - id-at-organizationalUnitName
internal const string Title = "2.5.4.12"; // 6.4.3 - id-at-title
internal const string Description = "2.5.4.13"; // 6.5.1 - id-at-description
internal const string BusinessCategory = "2.5.4.15"; // 6.5.4 - id-at-businessCategory
internal const string PostalCode = "2.5.4.17"; // 6.6.2 - id-at-postalCode
internal const string PostOfficeBox = "2.5.4.18"; // 6.6.3 - id-at-postOfficeBox
internal const string PhysicalDeliveryOfficeName = "2.5.4.19"; // 6.6.4 - id-at-physicalDeliveryOfficeName
internal const string TelephoneNumber = "2.5.4.20"; // 6.7.1 - id-at-telephoneNumber
internal const string X121Address = "2.5.4.24"; // 6.7.5 - id-at-x121Address
internal const string InternationalISDNNumber = "2.5.4.25"; // 6.7.6 - id-at-internationalISDNNumber
internal const string DestinationIndicator = "2.5.4.27"; // 6.7.8 - id-at-destinationIndicator
internal const string Name = "2.5.4.41"; // 6.2.1 - id-at-name
internal const string GivenName = "2.5.4.42"; // 6.2.4 - id-at-givenName
internal const string Initials = "2.5.4.43"; // 6.2.5 - id-at-initials
internal const string GenerationQualifier = "2.5.4.44"; // 6.2.6 - id-at-generationQualifier
internal const string DnQualifier = "2.5.4.46"; // 6.2.8 - id-at-dnQualifier
internal const string HouseIdentifier = "2.5.4.51"; // 6.3.7 - id-at-houseIdentifier
internal const string DmdName = "2.5.4.54"; // 6.11.1 - id-at-dmdName
internal const string Pseudonym = "2.5.4.65"; // 6.2.10 - id-at-pseudonym
internal const string UiiInUrn = "2.5.4.80"; // 6.13.3 - id-at-uiiInUrn
internal const string ContentUrl = "2.5.4.81"; // 6.13.4 - id-at-contentUrl
internal const string Uri = "2.5.4.83"; // 6.2.12 - id-at-uri
internal const string Urn = "2.5.4.86"; // 6.2.13 - id-at-urn
internal const string Url = "2.5.4.87"; // 6.2.14 - id-at-url
internal const string UrnC = "2.5.4.89"; // 6.12.4 - id-at-urnC
internal const string EpcInUrn = "2.5.4.94"; // 6.13.9 - id-at-epcInUrn
internal const string LdapUrl = "2.5.4.95"; // 6.13.10 - id-at-ldapUrl
internal const string OrganizationIdentifier = "2.5.4.97"; // 6.4.4 - id-at-organizationIdentifier
internal const string CountryOrRegionName3C = "2.5.4.98"; // 6.3.2 - id-at-countryCode3c
internal const string CountryOrRegionName3N = "2.5.4.99"; // 6.3.3 - id-at-countryCode3n
internal const string DnsName = "2.5.4.100"; // 6.2.15 - id-at-dnsName
internal const string IntEmail = "2.5.4.104"; // 6.2.16 - id-at-intEmail
internal const string JabberId = "2.5.4.105"; // 6.2.17 - id-at-jid

// RFC 2985
internal const string EmailAddress = "1.2.840.113549.1.9.1"; // B.3.5

// Cert Extensions
internal const string BasicConstraints = "2.5.29.10";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ namespace System.Security.Cryptography.X509Certificates
{
internal static partial class X500NameEncoder
{
private enum EncodingRules
{
Unknown,
IA5String,
DirectoryString,
PrintableString,
UTF8String,
NumericString,
}

private const string OidTagPrefix = "OID.";
private const string UseSemicolonSeparators = ";";
private const string UseCommaSeparators = ",";
Expand All @@ -20,6 +30,8 @@ internal static partial class X500NameEncoder
private static readonly SearchValues<char> s_needsQuotingChars =
SearchValues.Create(",+=\"\n<>#;"); // \r is NOT in this list, because it isn't in Windows.

private static readonly Lazy<Dictionary<string, EncodingRules>> s_lazyEncodingRulesLookup = new(CreateEncodingRulesLookup);

internal static string X500DistinguishedNameDecode(
byte[] encodedName,
bool printOid,
Expand Down Expand Up @@ -510,32 +522,35 @@ private static byte[] ParseRdn(ReadOnlySpan<char> tagOid, ReadOnlySpan<char> cha
throw new CryptographicException(SR.Cryptography_Invalid_X500Name, e);
}

if (tagOid.SequenceEqual(Oids.EmailAddress))
{
try
{
// An email address with an invalid value will throw.
writer.WriteCharacterString(UniversalTagNumber.IA5String, data);
}
catch (EncoderFallbackException)
{
throw new CryptographicException(SR.Cryptography_Invalid_IA5String);
}
}
else if (forceUtf8Encoding)
switch (LookupEncodingRules(tagOid))
{
writer.WriteCharacterString(UniversalTagNumber.UTF8String, data);
}
else
{
try
{
writer.WriteCharacterString(UniversalTagNumber.PrintableString, data);
}
catch (EncoderFallbackException)
{
writer.WriteCharacterString(UniversalTagNumber.UTF8String, data);
}
case EncodingRules.IA5String:
WriteCryptoCharacterString(writer, UniversalTagNumber.IA5String, data);
break;
case EncodingRules.UTF8String:
case EncodingRules.DirectoryString or EncodingRules.Unknown when forceUtf8Encoding:
WriteCryptoCharacterString(writer, UniversalTagNumber.UTF8String, data);
break;
case EncodingRules.NumericString:
WriteCryptoCharacterString(writer, UniversalTagNumber.NumericString, data);
break;
case EncodingRules.PrintableString:
WriteCryptoCharacterString(writer, UniversalTagNumber.PrintableString, data);
break;
case EncodingRules.DirectoryString:
case EncodingRules.Unknown:
try
{
writer.WriteCharacterString(UniversalTagNumber.PrintableString, data);
}
catch (EncoderFallbackException)
{
WriteCryptoCharacterString(writer, UniversalTagNumber.UTF8String, data);
}
break;
default:
Debug.Fail("Encoding rule was not handled.");
goto case EncodingRules.Unknown;
}
}

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

return written;
}

private static Dictionary<string, EncodingRules> CreateEncodingRulesLookup()
{
// Attributes that are not "obsolete" from ITU T-REC X.520-2019.
// Attributes that are included are attributes that are string-like and can be represented by a String.
// Windows does not have any restrictions on encoding non-string encodable types, it will encode them
// anyway, such as OID.2.5.4.14=test will encode test as a PrintableString, even though the OID is a SET.
// To maintain similar behavior as Windows, those types will remain treated as unknown.
const int LookupDictionarySize = 43;
Dictionary<string, EncodingRules> lookup = new(LookupDictionarySize, StringComparer.Ordinal)
{
{ Oids.KnowledgeInformation, EncodingRules.DirectoryString },
{ Oids.CommonName, EncodingRules.DirectoryString },
{ Oids.Surname, EncodingRules.DirectoryString },
{ Oids.SerialNumber, EncodingRules.PrintableString },
{ Oids.CountryOrRegionName, EncodingRules.PrintableString },
{ Oids.LocalityName, EncodingRules.DirectoryString },
{ Oids.StateOrProvinceName, EncodingRules.DirectoryString },
{ Oids.StreetAddress, EncodingRules.DirectoryString },
{ Oids.Organization, EncodingRules.DirectoryString },
{ Oids.OrganizationalUnit, EncodingRules.DirectoryString },
{ Oids.Title, EncodingRules.DirectoryString },
{ Oids.Description, EncodingRules.DirectoryString },
{ Oids.BusinessCategory, EncodingRules.DirectoryString },
{ Oids.PostalCode, EncodingRules.DirectoryString },
{ Oids.PostOfficeBox, EncodingRules.DirectoryString },
{ Oids.PhysicalDeliveryOfficeName, EncodingRules.DirectoryString },
{ Oids.TelephoneNumber, EncodingRules.PrintableString },
{ Oids.X121Address, EncodingRules.NumericString },
{ Oids.InternationalISDNNumber, EncodingRules.NumericString },
{ Oids.DestinationIndicator, EncodingRules.PrintableString },
{ Oids.Name, EncodingRules.DirectoryString },
{ Oids.GivenName, EncodingRules.DirectoryString },
{ Oids.Initials, EncodingRules.DirectoryString },
{ Oids.GenerationQualifier, EncodingRules.DirectoryString },
{ Oids.DnQualifier, EncodingRules.PrintableString },
{ Oids.HouseIdentifier, EncodingRules.DirectoryString },
{ Oids.DmdName, EncodingRules.DirectoryString },
{ Oids.Pseudonym, EncodingRules.DirectoryString },
{ Oids.UiiInUrn, EncodingRules.UTF8String },
{ Oids.ContentUrl, EncodingRules.UTF8String },
{ Oids.Uri, EncodingRules.UTF8String },
{ Oids.Urn, EncodingRules.UTF8String },
{ Oids.Url, EncodingRules.UTF8String },
{ Oids.UrnC, EncodingRules.PrintableString },
{ Oids.EpcInUrn, EncodingRules.DirectoryString },
{ Oids.LdapUrl, EncodingRules.UTF8String },
{ Oids.OrganizationIdentifier, EncodingRules.DirectoryString },
{ Oids.CountryOrRegionName3C, EncodingRules.PrintableString },
{ Oids.CountryOrRegionName3N, EncodingRules.NumericString },
{ Oids.DnsName, EncodingRules.UTF8String },
{ Oids.IntEmail, EncodingRules.UTF8String },
{ Oids.JabberId, EncodingRules.UTF8String },
{ Oids.EmailAddress, EncodingRules.IA5String },
};

Debug.Assert(lookup.Count == LookupDictionarySize);
return lookup;
}

private static void WriteCryptoCharacterString(AsnWriter writer, UniversalTagNumber tagNumber, ReadOnlySpan<char> data)
{
try
{
writer.WriteCharacterString(tagNumber, data);
}
catch (EncoderFallbackException)
{
if (tagNumber == UniversalTagNumber.IA5String)
{
throw new CryptographicException(SR.Cryptography_Invalid_IA5String);
}
else
{
throw new CryptographicException(SR.Cryptography_Invalid_X500Name);
}
}
}

private static EncodingRules LookupEncodingRules(ReadOnlySpan<char> oid)
{
Dictionary<string, EncodingRules> lookup = s_lazyEncodingRulesLookup.Value;
Dictionary<string, EncodingRules>.AlternateLookup<ReadOnlySpan<char>> alternateLookup =
lookup.GetAlternateLookup<ReadOnlySpan<char>>();
return alternateLookup.TryGetValue(oid, out EncodingRules rules) ? rules : EncodingRules.Unknown;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Formats.Asn1;
using Test.Cryptography;
using Xunit;

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

Assert.Equal(expected, formatted);
}

[Theory]
[InlineData("G=DotNet", UniversalTagNumber.UTF8String)]
[InlineData("L=Alexandria", UniversalTagNumber.UTF8String)]
[InlineData("O=GitHub", UniversalTagNumber.UTF8String)]
[InlineData("OU=ProdSec", UniversalTagNumber.UTF8String)]
[InlineData("S=Virginia", UniversalTagNumber.UTF8String)]
[InlineData("SN=Doe", UniversalTagNumber.UTF8String)]
[InlineData("ST=Main", UniversalTagNumber.UTF8String)]
[InlineData("T=Pancake", UniversalTagNumber.UTF8String)]
[InlineData("CN=Foo", UniversalTagNumber.UTF8String)]
[InlineData("I=DD", UniversalTagNumber.UTF8String)]
[InlineData("E=noone@example.com", UniversalTagNumber.IA5String)]
[InlineData("OID.2.5.4.11=ProdSec", UniversalTagNumber.UTF8String)]
[InlineData("OID.2.5.4.43=DD", UniversalTagNumber.UTF8String)]
[InlineData("OID.2.25.77135202736018529853602245419149860647=sample", UniversalTagNumber.UTF8String)]
[InlineData("C=US", UniversalTagNumber.PrintableString)]
[InlineData("OID.2.5.4.20=\"+0 (555) 555-1234\"", UniversalTagNumber.PrintableString)]
[InlineData("OID.2.5.4.99=840", UniversalTagNumber.NumericString, true)]
[InlineData("OID.2.5.4.98=USA", UniversalTagNumber.PrintableString, true)]
[InlineData("SERIALNUMBER=1234ABC", UniversalTagNumber.PrintableString)]
public static void Encode_ForceUtf8EncodingForEligibleComponents(
string distinguishedName,
UniversalTagNumber tagNumber,
bool nonWindowsOnly = false)
{
if (PlatformDetection.IsWindows && nonWindowsOnly)
{
return;
}

X500DistinguishedName name = new(distinguishedName, X500DistinguishedNameFlags.ForceUTF8Encoding);
byte[] encoded = name.RawData;

AsnValueReader reader = new(encoded, AsnEncodingRules.DER);
AsnValueReader component = reader.ReadSequence();
reader.ThrowIfNotEmpty();
AsnValueReader rdn = component.ReadSetOf();
component.ThrowIfNotEmpty();
AsnValueReader value = rdn.ReadSequence();
rdn.ThrowIfNotEmpty();

value.ReadObjectIdentifier();
Assert.Equal(new Asn1Tag(tagNumber), value.PeekTag());
}

[Theory]
[InlineData("C=$$")]
[InlineData("C=\"$$\"")]
[InlineData("E=\uD83C\uDF4C")] // banana
[InlineData("OID.2.5.4.99=a", true)]
[InlineData("OID.2.5.4.6=$$")]
public static void Encode_InvalidCharactersThrowCryptographicException(
string distinguishedName,
bool nonWindowsOnly = false)
{
if (PlatformDetection.IsWindows && nonWindowsOnly)
{
return;
}

Assert.ThrowsAny<CryptographicException>(() =>
new X500DistinguishedName(distinguishedName, X500DistinguishedNameFlags.ForceUTF8Encoding));
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows))]
[InlineData(X500DistinguishedNameFlags.None)]
[InlineData(X500DistinguishedNameFlags.ForceUTF8Encoding)]
public static void Encode_FailsForIncorrectSurrogatePair(X500DistinguishedNameFlags flags)
{
Assert.ThrowsAny<CryptographicException>(() => new X500DistinguishedName("CN=\uD800", flags));
}
}
}

0 comments on commit 6c83e0d

Please sign in to comment.