File: System\Security\Cryptography\X509Certificates\X500DistinguishedNameBuilder.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.Runtime.CompilerServices;
using System.Text;
 
namespace System.Security.Cryptography.X509Certificates
{
    /// <summary>
    /// This class facilitates building a distinguished name for an X.509 certificate.
    /// </summary>
    /// <remarks>
    /// When constructing the SEQUENCE OF Relative Distinguished Names, this builder
    /// constructs the SEQUENCE OF in the opposite order which they were added to the
    /// builder. For example:
    /// <code>
    /// builder.AddCommonName("Contoso");
    /// builder.AddCountryOrRegion("US");
    /// </code>
    /// Will produce a SEQUENCE OF with the country or region first, and the common
    /// name second. Because <see cref="X500DistinguishedName" /> follows RFC 4514
    /// when converting a Distinguished Name to a string, it starts with the last
    /// Relative Distinuished Name component, moving backward toward the first.
    /// Because the builder creates the SEQUENCE OF in reverse, and
    /// <see cref="X500DistinguishedName" /> is also in reverse, it gives the appearence
    /// of the added attributes and the string representation of the Distinguished Name in
    /// the same order.
    /// </remarks>
    public sealed class X500DistinguishedNameBuilder
    {
        private readonly List<byte[]> _encodedComponents = new List<byte[]>();
        private readonly AsnWriter _writer = new AsnWriter(AsnEncodingRules.DER);
 
        /// <summary>
        /// Adds a Relative Distinguished Name attribute identified by an OID.
        /// </summary>
        /// <param name="oidValue">The OID of the attribute.</param>
        /// <param name="value">The value of the attribute.</param>
        /// <param name="stringEncodingType">
        /// The encoding type to use when encoding the <paramref name="value" />
        /// in to the attribute.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="oidValue" /> or <paramref name="value" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <para>
        ///   <paramref name="oidValue" /> is an empty string or not a valid OID.
        /// </para>
        /// <para>-or-</para>
        /// <para>
        ///   <paramref name="stringEncodingType" /> is not a type for character strings.
        /// </para>
        /// <para>-or-</para>
        /// <para>
        ///   <paramref name="value" /> is not encodable as defined by <paramref name="stringEncodingType" />.
        /// </para>
        /// </exception>
        public void Add(string oidValue, string value, UniversalTagNumber? stringEncodingType = null)
        {
            ArgumentNullException.ThrowIfNull(value);
            ArgumentException.ThrowIfNullOrEmpty(oidValue);
 
            UniversalTagNumber tag = GetAndValidateTagNumber(stringEncodingType);
            EncodeComponent(oidValue, value, tag);
        }
 
        /// <summary>
        /// Adds a Relative Distinguished Name attribute identified by an OID.
        /// </summary>
        /// <param name="oid">The OID of the attribute.</param>
        /// <param name="value">The value of the attribute.</param>
        /// <param name="stringEncodingType">
        /// The encoding type to use when encoding the <paramref name="value" />
        /// in to the attribute.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="oid" /> or <paramref name="value" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <para>
        ///   <paramref name="oid" /> does not contain a valid OID.
        /// </para>
        /// <para>-or-</para>
        /// <para>
        ///   <paramref name="stringEncodingType" /> is not a type for character strings.
        /// </para>
        /// <para>-or-</para>
        /// <para>
        ///   <paramref name="value" /> is not encodable as defined by <paramref name="stringEncodingType" />.
        /// </para>
        /// </exception>
        public void Add(Oid oid, string value, UniversalTagNumber? stringEncodingType = null)
        {
            ArgumentNullException.ThrowIfNull(oid);
            ArgumentNullException.ThrowIfNull(value);
            ArgumentException.ThrowIfNullOrEmpty(oid.Value);
 
            UniversalTagNumber tag = GetAndValidateTagNumber(stringEncodingType);
            EncodeComponent(oid.Value, value, tag);
        }
 
        /// <summary>
        /// Adds an email address attribute.
        /// </summary>
        /// <param name="emailAddress">The email address to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="emailAddress" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="emailAddress" /> is empty or exceeds 255 characters.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 1.2.840.113549.1.9.1 as an IA5String.
        /// </remarks>
        public void AddEmailAddress(string emailAddress)
        {
            //RFC 2585:
            // emailAddress ATTRIBUTE ::= {
            //   WITH SYNTAX IA5String (SIZE(1..pkcs-9-ub-emailAddress))
            //   EQUALITY MATCHING RULE pkcs9CaseIgnoreMatch
            //   ID pkcs-9-at-emailAddress
            // }
            // pkcs-9-ub-pkcs9String  INTEGER ::= 255
            // pkcs-9-ub-emailAddress INTEGER ::= pkcs-9-ub-pkcs9String
 
            // We don't attempt to do any input validation that the email resembles an email
            // address, only that it conforms to the ASN.1 syntax.
 
            ArgumentException.ThrowIfNullOrEmpty(emailAddress);
 
            if (emailAddress.Length > 255)
            {
                throw new ArgumentException(SR.Argument_X500_EmailTooLong, nameof(emailAddress));
            }
 
            EncodeComponent(Oids.EmailAddress, emailAddress, UniversalTagNumber.IA5String);
        }
 
        /// <summary>
        /// Adds a common name attribute.
        /// </summary>
        /// <param name="commonName">The common name to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="commonName" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="commonName" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 2.5.4.3 as a UTF8String.
        /// </remarks>
        public void AddCommonName(string commonName)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-commonName
            // WITH SYNTAX UnboundedDirectoryString
 
            ArgumentException.ThrowIfNullOrEmpty(commonName);
            EncodeComponent(Oids.CommonName, commonName, UniversalTagNumber.UTF8String);
        }
 
        /// <summary>
        /// Adds a locality name attribute.
        /// </summary>
        /// <param name="localityName">The locality name to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="localityName" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="localityName" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 2.5.4.7 as a UTF8String.
        /// </remarks>
        public void AddLocalityName(string localityName)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-localityName
            // WITH SYNTAX UnboundedDirectoryString
 
            ArgumentException.ThrowIfNullOrEmpty(localityName);
            EncodeComponent(Oids.LocalityName, localityName, UniversalTagNumber.UTF8String);
        }
 
        /// <summary>
        /// Adds a country or region attribute.
        /// </summary>
        /// <param name="twoLetterCode">The two letter code of the country or region.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="twoLetterCode" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="twoLetterCode" /> is not exactly two characters, or contains
        /// characters that are not A through Z.
        /// </exception>
        /// <remarks>
        /// <para>This encodes an attribute with the OID 2.5.4.6 as a PrintableString.</para>
        /// <para>
        /// <paramref name="twoLetterCode" /> should be a two letter ISO 3166 alpha-2 code, and
        /// will be normalized to upper case characters.
        /// </para>
        /// </remarks>
        public void AddCountryOrRegion(string twoLetterCode)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-countryName
            // WITH SYNTAX CountryName
            // CountryName ::= PrintableString(SIZE (2))
            // An attribute value for country name is a string chosen from ISO 3166-1 alpha-2 or ISO 3166-3 alpha-2.
            // We can't reasonably enforce the ISO 3166 list, but we can ensure it's a two letter ISO code
            // and consists of alpha characters.
 
            ArgumentException.ThrowIfNullOrEmpty(twoLetterCode);
            ReadOnlySpan<char> twoLetterCodeSpan = twoLetterCode;
 
            // This could be a surrogate pair, but since we are encoding as a PrintableString,
            // those will be prohibited, so "Length" should be fine for checking the length of
            // the string.
            // Input must be A-Z per ISO 3166.
            if (twoLetterCode.Length != 2 || !char.IsAsciiLetter(twoLetterCode[0]) || !char.IsAsciiLetter(twoLetterCode[1]))
            {
                throw new ArgumentException(SR.Argument_X500_InvalidCountryOrRegion, nameof(twoLetterCode));
            }
 
            Span<char> fixupTwoLetterCode = stackalloc char[2];
            int written = twoLetterCodeSpan.ToUpperInvariant(fixupTwoLetterCode);
            Debug.Assert(written == 2);
 
            EncodeComponent(
                Oids.CountryOrRegionName,
                fixupTwoLetterCode,
                UniversalTagNumber.PrintableString,
                nameof(twoLetterCode));
        }
 
        /// <summary>
        /// Adds an organization name attribute.
        /// </summary>
        /// <param name="organizationName">The organization name to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="organizationName" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="organizationName" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 2.5.4.10 as a UTF8String.
        /// </remarks>
        public void AddOrganizationName(string organizationName)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-organizationName
            // WITH SYNTAX UnboundedDirectoryString
 
            ArgumentException.ThrowIfNullOrEmpty(organizationName);
            EncodeComponent(Oids.Organization, organizationName, UniversalTagNumber.UTF8String);
        }
 
        /// <summary>
        /// Adds an organizational unit name attribute.
        /// </summary>
        /// <param name="organizationalUnitName">The organizational unit name to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="organizationalUnitName" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="organizationalUnitName" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 2.5.4.11 as a UTF8String.
        /// </remarks>
        public void AddOrganizationalUnitName(string organizationalUnitName)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-organizationalUnitName
            // WITH SYNTAX UnboundedDirectoryString
 
            ArgumentException.ThrowIfNullOrEmpty(organizationalUnitName);
            EncodeComponent(Oids.OrganizationalUnit, organizationalUnitName, UniversalTagNumber.UTF8String);
        }
 
        /// <summary>
        /// Adds a state or province name attribute.
        /// </summary>
        /// <param name="stateOrProvinceName">The state or province name to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="stateOrProvinceName" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="stateOrProvinceName" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 2.5.4.8 as a UTF8String.
        /// </remarks>
        public void AddStateOrProvinceName(string stateOrProvinceName)
        {
            // ITU T-REC X.520 Annex A:
            // id-at-stateOrProvinceName
            // WITH SYNTAX UnboundedDirectoryString
 
            ArgumentException.ThrowIfNullOrEmpty(stateOrProvinceName);
            EncodeComponent(Oids.StateOrProvinceName, stateOrProvinceName, UniversalTagNumber.UTF8String);
        }
 
        /// <summary>
        /// Adds a domain component attribute.
        /// </summary>
        /// <param name="domainComponent">The domain component to add.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="domainComponent" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="domainComponent" /> is empty.
        /// </exception>
        /// <remarks>
        /// This encodes an attribute with the OID 0.9.2342.19200300.100.1.25 as an IA5String.
        /// </remarks>
        public void AddDomainComponent(string domainComponent)
        {
            // RFC 5912
            // id-domainComponent      AttributeType ::=
            //  { itu-t(0) data(9) pss(2342) ucl(19200300) pilot(100)
            //  pilotAttributeType(1) 25 }
            //  at-domainComponent ATTRIBUTE ::= {TYPE IA5String
            //  IDENTIFIED BY id-domainComponent }
 
            ArgumentException.ThrowIfNullOrEmpty(domainComponent);
            EncodeComponent(Oids.DomainComponent, domainComponent, UniversalTagNumber.IA5String);
        }
 
        /// <summary>
        /// Builds an <see cref="X500DistinguishedName" /> that represents the encoded attributes.
        /// </summary>
        /// <returns>
        /// An <see cref="X500DistinguishedName" /> that represents the encoded attributes.
        /// </returns>
        public X500DistinguishedName Build()
        {
            _writer.Reset();
 
            using (_writer.PushSequence())
            {
                for (int i = _encodedComponents.Count - 1; i >= 0; i--)
                {
                    _writer.WriteEncodedValue(_encodedComponents[i]);
                }
            }
 
            return _writer.Encode(static encoded => new X500DistinguishedName(encoded));
        }
 
        private void EncodeComponent(
            string oid,
            ReadOnlySpan<char> value,
            UniversalTagNumber stringEncodingType,
            [CallerArgumentExpression(nameof(value))] string? paramName = null)
        {
            _writer.Reset();
 
            using (_writer.PushSetOf())
            using (_writer.PushSequence())
            {
                _writer.WriteObjectIdentifier(oid);
 
                try
                {
                    _writer.WriteCharacterString(stringEncodingType, value);
                }
                catch (EncoderFallbackException)
                {
                    throw new ArgumentException(SR.Format(SR.Argument_Asn1_InvalidStringContents, stringEncodingType), paramName);
                }
            }
 
            _encodedComponents.Add(_writer.Encode());
        }
 
        private static UniversalTagNumber GetAndValidateTagNumber(UniversalTagNumber? stringEncodingType)
        {
            switch (stringEncodingType)
            {
                case null:
                    return UniversalTagNumber.UTF8String;
                case UniversalTagNumber.UTF8String:
                case UniversalTagNumber.NumericString:
                case UniversalTagNumber.PrintableString:
                case UniversalTagNumber.IA5String:
                case UniversalTagNumber.VisibleString:
                case UniversalTagNumber.BMPString:
                case UniversalTagNumber.T61String:
                    return stringEncodingType.GetValueOrDefault();
                default:
                    throw new ArgumentException(SR.Argument_Asn1_InvalidCharacterString, nameof(stringEncodingType));
            }
        }
    }
}