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
- RFC 5280 sections 4.1.2.4 and 4.2.1.6
- Microsoft AsnReader.ReadSequence
- Microsoft X509SubjectAlternativeNameExtension
Original links in historical context
Original links are retained for attribution. They do not endorse the old code; current availability was not verified.