פענוח רשומות directoryName בהרחבת SAN של תעודה
הקוד להמחשה ולא הורץ.
המקור האנגלי פורסם בתאריך 2023-11-07T09:46:00.003+02:00. המאמר המקורי באנגלית
האיור אינו זמין: איור בנושא תעודות מהמאמר המקורי (מקור התמונה המקורי).
רשומת directoryName ב־SAN היא שם מובחן של X.500, ולא כתובת שרת ספרייה. בבחירה GeneralName היא משתמשת בתג בנוי מסוג context-specific שמספרו [4], העוטף ערך Name. בתוך Name יש רצף של שמות מובחנים יחסיים, שכל אחד מהם הוא SET של רצפי מאפיינים. הקוד הארכיוני קורא בטעות את [4] בתור SET ומתייג מחדש שם מקודד שלם כאילו היה givenName או surname. [1]
התחליף שלהלן מחזיר את כל המאפיינים מכל רשומות directoryName. הוא בודק את מחלקת התג ואת צורתו הבנויה, שומר קוראים מקוננים נפרדים, מגביל את גודל הקלט ואת מספר הפריטים ודוחה נתונים עודפים. ה־OID של givenName הוא 2.5.4.42 ושל surname הוא 2.5.4.4; יש לבחור OID אלו אחרי הפענוח, תוך שמירה על ערכים מרובים או חסרים במקום להניח שקיים ערך יחיד. הקוד אינו מאמת את התעודה ואינו מוכיח את זהותו של אדם. [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;
}
}זהו תחביר C# מודרני ומפענח תצוגה מוגבל במכוון, ולא טענה שאותו קובץ מתקמפל ללא שינוי בכל גרסת יעד ישנה של .NET Framework. יש לבחור חבילת System.Formats.Asn1 תואמת ולהתאים במכוון ממשקים התלויים בגרסת היעד. קידודי מחרוזת שאינם נתמכים מוחזרים עם התחילית המפורשת #DER: במקום לנחש את תוכנם. יש לבצע escaping לערכי תצוגה לפי אמצעי הפלט, ואין להשוות מחרוזות מעוצבות כתחליף להשוואת שמות X.500. [2]
X509SubjectAlternativeNameExtension מציעה מעבר נוח על שמות DNS ועל כתובות IP, אך מתודות אלו אינן מפענח כללי של מאפייני directoryName. לכן אין להסיק מהאזכור המקורי של .NET 7 או 8 שהמחלקה המובנית שולפת givenName ו־surname. המאמר הקודם על Bouncy Castle נשמר כקישור היסטורי קשור; הוא אינו משמש ראיה לתקינות הקוד שבו. [3]
מקורות ראשוניים
- RFC 5280 sections 4.1.2.4 and 4.2.1.6
- Microsoft AsnReader.ReadSequence
- Microsoft X509SubjectAlternativeNameExtension
קישורי המקור בהקשר היסטורי
הקישורים המקוריים נשמרו לצורכי ייחוס. הם אינם אישור לתקינות הקוד הישן; זמינותם הנוכחית לא אומתה.