Skip to content

Parse directoryName entries in a certificate SAN extension

Parse directoryName entries in a certificate SAN extension

Illustrative code, not executed.

Original English publication date: 2023-11-07T09:46:00.003+02:00. Original English article

Illustration unavailable: Certificate illustration from the original article (original image source).

A SAN directoryName is an X.500 distinguished name, not a directory-server address. In the GeneralName choice, it uses context-specific constructed tag [4] around a Name value. Inside that Name are a sequence of relative distinguished names, each a SET of attribute sequences. The archived code incorrectly reads [4] as a SET and relabels an entire encoded name as givenName or surname. [1]

The replacement below returns all attributes from all directoryName entries. It checks the tag class and constructed form, keeps nested readers separate, enforces input/count bounds and rejects trailing data. givenName is OID 2.5.4.42 and surname is 2.5.4.4; select those OIDs after parsing, retaining multiple or missing values instead of assuming one value exists. It does not validate the certificate or establish a person’s identity. [1][2]

using System;
using System.Collections.Generic;
using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

public static class DirectorySanExample
{
    // Illustrative parser; no certificate was loaded or parsed for this article.
    public static List<(int Name, int Rdn, string Oid, string DisplayValue)> Parse(X509Certificate2 cert)
    {
        if (cert == null) throw new ArgumentNullException(nameof(cert));
        X509Extension? san = null;
        foreach (X509Extension extension in cert.Extensions)
        {
            if (extension.Oid?.Value != "2.5.29.17") continue;
            if (san != null) throw new CryptographicException("Duplicate SAN extensions.");
            san = extension;
        }
        var output = new List<(int, int, string, string)>();
        if (san == null) return output;
        byte[] raw = san.RawData;
        if (raw.Length == 0 || raw.Length > 65536)
            throw new CryptographicException("SAN size exceeds this parser's policy.");
        var outer = new AsnReader(raw, AsnEncodingRules.DER);
        var names = outer.ReadSequence();
        outer.ThrowIfNotEmpty();
        if (!names.HasData) throw new CryptographicException("Empty GeneralNames.");
        int nameIndex = 0;
        while (names.HasData)
        {
            if (++nameIndex > 128) throw new CryptographicException("Too many names.");
            Asn1Tag tag = names.PeekTag();
            if (tag.TagClass != TagClass.ContextSpecific || tag.TagValue > 8)
                throw new CryptographicException("Unexpected GeneralName tag.");
            if (tag.TagValue != 4) { names.ReadEncodedValue(); continue; }
            if (!tag.IsConstructed) throw new CryptographicException("directoryName must be constructed.");
            var explicitName = names.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 4, true));
            var rdns = explicitName.ReadSequence();
            explicitName.ThrowIfNotEmpty();
            int rdnIndex = 0;
            while (rdns.HasData)
            {
                if (++rdnIndex > 128) throw new CryptographicException("Too many RDNs.");
                var rdn = rdns.ReadSetOf(); // DER SET ordering is validated.
                if (!rdn.HasData) throw new CryptographicException("Empty RDN.");
                while (rdn.HasData)
                {
                    if (output.Count >= 256) throw new CryptographicException("Too many attributes.");
                    var attribute = rdn.ReadSequence();
                    string oid = attribute.ReadObjectIdentifier();
                    ReadOnlyMemory<byte> encodedValue = attribute.ReadEncodedValue();
                    attribute.ThrowIfNotEmpty();
                    output.Add((nameIndex, rdnIndex, oid, Display(encodedValue)));
                }
            }
        }
        return output;
    }

    private static string Display(ReadOnlyMemory<byte> encoded)
    {
        var reader = new AsnReader(encoded, AsnEncodingRules.DER);
        Asn1Tag tag = reader.PeekTag();
        // A deliberately limited display decoder; other encodings remain explicitly DER.
        if (tag.TagClass != TagClass.Universal || tag.IsConstructed ||
            (tag.TagValue != (int)UniversalTagNumber.UTF8String &&
             tag.TagValue != (int)UniversalTagNumber.PrintableString &&
             tag.TagValue != (int)UniversalTagNumber.BMPString))
            return "#DER:" + Convert.ToHexString(encoded.Span);
        string value = reader.ReadCharacterString((UniversalTagNumber)tag.TagValue);
        reader.ThrowIfNotEmpty();
        if (value.Length > 4096) throw new CryptographicException("Attribute text too long.");
        return value;
    }
}

This is modern C# syntax and a deliberately limited display decoder, not a claim that the same file compiles unchanged on every older .NET Framework target. Choose a compatible System.Formats.Asn1 package and adjust target-specific APIs deliberately. Unsupported string encodings are returned with an explicit #DER: prefix rather than guessed. Escape display values for the output medium, and do not compare formatted strings as a substitute for X.500 name matching. [2]

X509SubjectAlternativeNameExtension exposes convenient DNS-name and IP-address enumeration, but those methods are not a general directoryName attribute parser. The original reference to .NET 7 or 8 should therefore not imply that the built-in class extracts givenName and surname. The earlier Bouncy Castle article is retained as a historical related link; it has not been adopted as evidence that its code is correct. [3]

Primary references

  1. RFC 5280 sections 4.1.2.4 and 4.2.1.6
  2. Microsoft AsnReader.ReadSequence
  3. Microsoft X509SubjectAlternativeNameExtension

Original links are retained for attribution. They do not endorse the old code; current availability was not verified.

Post a Comment

Previous Post Next Post