File: System\Security\Cryptography\X509Certificates\X500NameEncoder.ManagedDecode.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Formats.Asn1;
using System.Text;
 
namespace System.Security.Cryptography.X509Certificates
{
    internal static partial class X500NameEncoder
    {
        private static string X500DistinguishedNameDecode(
            byte[] encodedName,
            bool printOid,
            bool reverse,
            bool quoteIfNeeded,
            string dnSeparator,
            string multiValueSeparator,
            bool addTrailingDelimiter)
        {
            try
            {
                AsnReader x500NameReader = new AsnReader(encodedName, AsnEncodingRules.DER);
                AsnReader x500NameSequenceReader = x500NameReader.ReadSequence();
                var rdnReaders = new List<AsnReader>();
 
                x500NameReader.ThrowIfNotEmpty();
 
                while (x500NameSequenceReader.HasData)
                {
                    // To match Windows' behavior, permit multi-value RDN SETs to not
                    // be DER sorted.
                    rdnReaders.Add(x500NameSequenceReader.ReadSetOf(skipSortOrderValidation: true));
                }
 
                // We need to use a ValueStringBuilder to hold the data as we're building it, and there's the usual
                // arbitrary process of choosing a number that's "big enough" to minimize reallocations without wasting
                // too much space in the average case.
                //
                // So, let's look at an example of what our output might be.
                //
                // GitHub.com's SSL cert has a "pretty long" subject (partially due to the unknown OIDs):
                //   businessCategory=Private Organization
                //   1.3.6.1.4.1.311.60.2.1.3=US
                //   1.3.6.1.4.1.311.60.2.1.2=Delaware
                //   serialNumber=5157550
                //   street=548 4th Street
                //   postalCode=94107
                //   C=US
                //   ST=California
                //   L=San Francisco
                //   O=GitHub, Inc.
                //   CN=github.com
                //
                // Which comes out to 228 characters using OpenSSL's default pretty-print
                // (openssl x509 -in github.cer -text -noout)
                // Throw in some "maybe-I-need-to-quote-this" quotes, and a couple of extra/extra-long O/OU values
                // and round that up to the next programmer number, and you get that 512 should avoid reallocations
                // in all but the most dire of cases.
                ValueStringBuilder decodedName = new ValueStringBuilder(stackalloc char[512]);
                int entryCount = rdnReaders.Count;
                bool printSpacing = false;
 
                for (int i = 0; i < entryCount; i++)
                {
                    int loc = reverse ? entryCount - i - 1 : i;
 
                    // RelativeDistinguishedName ::=
                    //   SET SIZE (1..MAX) OF AttributeTypeAndValue
                    //
                    // AttributeTypeAndValue::= SEQUENCE {
                    //   type AttributeType,
                    //   value    AttributeValue }
                    //
                    // AttributeType::= OBJECT IDENTIFIER
                    //
                    // AttributeValue ::= ANY-- DEFINED BY AttributeType
 
                    if (printSpacing)
                    {
                        decodedName.Append(dnSeparator);
                    }
                    else
                    {
                        printSpacing = true;
                    }
 
                    AsnReader rdnReader = rdnReaders[loc];
                    bool hadValue = false;
 
                    while (rdnReader.HasData)
                    {
                        AsnReader tavReader = rdnReader.ReadSequence();
                        string oid = tavReader.ReadObjectIdentifier();
                        string attributeValue = ReadAttributeValue(tavReader, out bool fallback);
 
                        tavReader.ThrowIfNotEmpty();
 
                        if (hadValue)
                        {
                            decodedName.Append(multiValueSeparator);
                        }
                        else
                        {
                            hadValue = true;
                        }
 
                        if (printOid)
                        {
                            AppendOid(ref decodedName, oid);
                        }
 
                        bool quote = quoteIfNeeded && NeedsQuoting(attributeValue) && !fallback;
 
                        if (quote)
                        {
                            decodedName.Append('"');
 
                            // If the RDN itself had a quote within it, that quote needs to be escaped
                            // with another quote.
                            attributeValue = attributeValue.Replace("\"", "\"\"");
                        }
 
                        decodedName.Append(attributeValue);
 
                        if (quote)
                        {
                            decodedName.Append('"');
                        }
                    }
                }
 
                if (addTrailingDelimiter && decodedName.Length > 0)
                {
                    decodedName.Append(dnSeparator);
                }
 
                string name = decodedName.ToString();
                decodedName.Dispose();
                return name;
            }
            catch (AsnContentException e)
            {
                throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
            }
        }
 
        private static string ReadAttributeValue(AsnReader tavReader, out bool binaryFallback)
        {
            Debug.Assert(tavReader.RuleSet == AsnEncodingRules.DER);
 
            Asn1Tag tag = tavReader.PeekTag();
 
            if (tag.TagClass == TagClass.Universal)
            {
                switch ((UniversalTagNumber)tag.TagValue)
                {
                    case UniversalTagNumber.BMPString:
                    case UniversalTagNumber.IA5String:
                    case UniversalTagNumber.NumericString:
                    case UniversalTagNumber.PrintableString:
                    case UniversalTagNumber.UTF8String:
                    case UniversalTagNumber.T61String:
                        // .NET's string comparisons start by checking the length, so a trailing
                        // NULL character which was literally embedded in the DER would cause a
                        // failure in .NET whereas it wouldn't have with strcmp.
                        binaryFallback = false;
                        return tavReader.ReadCharacterString((UniversalTagNumber)tag.TagValue).TrimEnd('\0');
                    case UniversalTagNumber.OctetString:
                        // Windows will implicitly unwrap one OCTET STRING and display only the contents.
                        if (tavReader.TryReadPrimitiveOctetString(out ReadOnlyMemory<byte> contents))
                        {
                            binaryFallback = true;
                            return BinaryEncode(contents);
                        }
 
                        Debug.Fail("TryReadPrimitiveOctetString should either succeed or throw with DER.");
                        throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
                }
            }
 
            binaryFallback = true;
            return BinaryEncode(tavReader.ReadEncodedValue());
 
            static string BinaryEncode(ReadOnlyMemory<byte> data)
            {
                return string.Create(1 + data.Length * 2, data, static (buff, state) =>
                {
                    buff[0] = '#';
                    HexConverter.EncodeToUtf16(state.Span, buff.Slice(1));
                });
            }
        }
    }
}