Эквивалентные X509Name с разными кодировками DER в Bouncy Castle
Узнайте, почему объекты X509Name в Bouncy Castle показываются эквивалентными, но имеют разные кодировки DER при сравнении имен издателей из подписей XAdES и сертификатов. Узнайте основные причины и практические решения.
Почему объекты X509Name в Bouncy Castle (C#) показываются эквивалентными, но имеют разные кодировки DER при сравнении имен издателей из подписи XAdES и встроенного сертификата?
Я создаю подпись XAdES с помощью Bouncy Castle в C# и мне нужно включить имя издателя сертификата в XML в соответствии со схемой XAdES 1.3.2. Схема требует использования элемента xades:SigningCertificate (не xades:SigningCertificateV2), который содержит элемент ds:X509IssuerName.
Например, структура XML должна включать:
xades:SigningCertificate
xades:Cert
xades:CertDigest
<ds:DigestMethod Algorithm=“http://www.w3.org/2001/04/xmlenc#sha256”/>
ds:DigestValue…</ds:DigestValue>
</xades:CertDigest>
xades:IssuerSerial
ds:X509IssuerNameDC=local,DC=test,CN=Test CA</ds:X509IssuerName>
ds:X509SerialNumber…</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
Значение ds:X509IssuerName получается из signingCert.IssuerDN.ToString(), где signingCert - это объект X509Certificate из Bouncy Castle.
Во время проверки подписи мне нужно убедиться, что сертификат, на который ссылается элемент xades:SigningCertificate, соответствует тому, что находится в части XML подписи. Однако при сравнении:
X509Name signingCertIssuerDN = new X509Name(_issuerSerial.X509IssuerName);
Byte[] signingCertIssuerBytes = signingCertIssuerDN.GetEncoded();
X509Certificate xmldsigCert = new X509Certificate(Convert.FromBase64String(_keyInfoXml.InnerText));
X509Name dsigIssuerDN = dsigCert.IssuerDN;
Byte[] dsigIssuerBytes = dsigIssuerDN.GetEncoded();
Boolean isEquivalent = dsigIssuerDN.Equivalent(certIssuerDN); // true
Boolean isEncodedSame = dsigIssuerBytes.SequenceEqual(signingCertIssuerBytes); // false
Что вызывает это расхождение, и является ли нормой, чтобы объекты X509Name, инициализированные из строки, отличались от тех, что считываются из объекта X509Certificate, в отношении кодировки DER?
Объекты X509Name в Bouncy Castle: эквивалентность и различия в кодировке DER
Объекты X509Name в Bouncy Castle часто оказываются эквивалентными при проверке через метод Equivalent(), но при этом имеют разные кодировки DER из-за различий в форматировании имен, порядке атрибутов и правилах кодирования. Это распространенное явление при сравнении имен издателей из разных источников - одного из исходной DER-кодированной distinguished name (DN) сертификата и другого из строкового представления, преобразованного обратно в объект X509Name.
Содержание
- Понимание расхождения в X509Name
- Основные причины различий в кодировке DER
- Сравнение методов создания X509Name
- Практические решения и обходные пути
- Лучшие практики обработки имени издателя в XAdES
- Примеры кода для правильного сравнения
- Заключение
Понимание расхождения в X509Name
Проблема, с которой вы столкнулись, на самом деле довольно распространена при обработке сертификатов X.509. Когда вы получаете имя издателя из сертификата с помощью signingCert.IssuerDN.ToString(), Bouncy Castle создает строковое представление distinguished name (DN). Эта строка обычно следует формату RFC 4514, который является удобочитаемым и стандартизированным для операций с каталогами.
Однако при разборе этой строки обратно в объект X509Name с помощью new X509Name(_issuerSerial.X509IssuerName) процесс разбора может интерпретировать и закодировать имя иначе, чем исходный DN издателя сертификата. Это происходит потому, что:
- Исходный DN издателя сертификата был закодирован непосредственно из его DER-структуры
- Строковое представление проходит через процесс кодирования → декодирования → повторного кодирования
- Строковые представления часто нормализуют или переформатируют компоненты имен
Согласно документации Bouncy Castle, объекты X509Name считаются эквивалентными на основе их логического содержимого, а не точного байтового представления. Именно поэтому метод Equivalent() возвращает true, даже когда кодировки DER различаются.
Основные причины различий в кодировке DER
1. Различия в порядке атрибутов
Distinguished name X.509 технически представляют собой неупорядоченные коллекции relative distinguished names (RDN). Однако различные реализации и методы разбора могут упорядочивать атрибуты по-разному. Например:
// Исходный сертификат может содержать: DC=local,DC=test,CN=Test CA
// Строковое представление может быть разобрано как: CN=Test CA,DC=local,DC=test
Это повторное упорядочение семантически эквивалентно, но создает разные байтовые последовательности DER.
2. Нормализация формата строки
Когда Bouncy Castle преобразует X509Name в строку с помощью ToString(), он применяет различные правила нормализации:
- Обработка пробельных символов
- Экранирование символов
- Форматирование значений атрибутов
- Правила кавычек для специальных символов
Эти нормализации не обязательно обратимы, что приводит к различиям в кодировке при обратном разборе.
3. Кодирование строковых значений
Различные подходы к кодированию строк могут вызывать различия на уровне байтов:
// Пример: различия в кодировании Unicode vs ASCII
// Unicode: UTF-16 с BOM или явная кодировка
// Кодировка DER: UTF-8 для большинства строковых атрибутов
4. Вариации кодирования BER vs DER
Хотя DER должен быть строгой канонической кодировкой, некоторые реализации могут создавать слегка разные кодировки для одной и той же логической структуры из-за:
- Различных подходов к кодированию длины
- Обработки необязательных элементов
- Вариаций номеров тегов
Сравнение методов создания X509Name
Рассмотрим два разных пути, которые приводят к вашему сравнению:
Путь 1: Из сертификата (исходный DER)
X509Certificate xmldsigCert = new X509Certificate(Convert.FromBase64String(_keyInfoXml.InnerText));
X509Name dsigIssuerDN = dsigCert.IssuerDN;
Byte[] dsigIssuerBytes = dsigIssuerDN.GetEncoded();
Этот путь:
- Читает сертификат непосредственно из DER-закодированных байтов
- Извлекает DN издателя так, как он был изначально закодирован
- Сохраняет точную байтовую последовательность из сертификата
Путь 2: Из строки (реконструированный)
X509Name signingCertIssuerDN = new X509Name(_issuerSerial.X509IssuerName);
Byte[] signingCertIssuerBytes = signingCertIssuerDN.GetEncoded();
Этот путь:
- Принимает удобочитаемое строковое представление
- Разбирает и реконструирует объект X509Name
- Повторно кодирует его в формат DER, потенциально с различиями
Ключевое отличие заключается в том, что первый путь сохраняет исходную кодировку, тогда как второй путь проходит через промежуточный удобочитаемый формат, который вносит вариации.
Практические решения и обходные пути
Решение 1: Использование исходных байтов сертификата для сравнения
Вместо сравнения реконструированных имен издателей, сравнивайте сертификаты непосредственно:
// Извлекаем исходные байты DN издателя из обоих сертификатов
byte[] cert1IssuerBytes = cert1.GetIssuerX509Principal().GetEncoded();
byte[] cert2IssuerBytes = cert2.GetIssuerX509Principal().GetEncoded();
// Сравниваем исходные кодировки
bool isSameIssuer = cert1IssuerBytes.SequenceEqual(cert2IssuerBytes);
Решение 2: Нормализация перед сравнением
Создайте функцию нормализации, обеспечивающую последовательное кодирование:
public static byte[] GetNormalizedIssuerBytes(X509Certificate cert)
{
// Получаем DN издателя и создаем каноническое представление
X509Name issuerDN = cert.IssuerDN;
// Преобразуем в строку и обратно для обеспечения последовательного форматирования
string canonicalString = issuerDN.ToString();
X509Name canonicalDN = new X509Name(canonicalString);
return canonicalDN.GetEncoded();
}
// Затем сравниваем с использованием нормализованных версий
byte[] normalizedIssuer1 = GetNormalizedIssuerBytes(cert1);
byte[] normalizedIssuer2 = GetNormalizedIssuerBytes(cert2);
bool isSame = normalizedIssuer1.SequenceEqual(normalizedIssuer2);
Решение 3: Использование прямого сравнения сертификатов
Для проверки XAdES вы можете сравнивать весь сертификат, а не только имя издателя:
bool CertificatesMatch(X509Certificate cert1, X509Certificate cert2)
{
// Сравниваем серийные номера и DN издателей с помощью Equivalent()
return cert1.SerialNumber.Equals(cert2.SerialNumber) &&
cert1.IssuerDN.Equivalent(cert2.IssuerDN);
}
Лучшие практики обработки имени издателя в XAdES
1. Сохраняйте исходные кодировки
При работе с подписями XAdES всегда старайтесь сохранять исходные DER-закодированные значения из сертификатов, а не преобразовывать их в строковые представления и обратно.
2. Используйте Equivalent() для логического сравнения
Для большинства целей проверки метод Equivalent() достаточен и более надежен, чем сравнение на уровне байтов:
bool isValidIssuer = dsigIssuerDN.Equivalent(signingCertIssuerDN);
3. Обрабатывайте строковые представления с осторожностью
Если вы должны использовать строковые представления для вывода XML:
// Используйте последовательный метод форматирования
string issuerNameXml = FormatIssuerNameForXml(cert.IssuerDN);
private string FormatIssuerNameForXml(X509Name issuerDN)
{
// Используйте последовательное форматирование без лишних пробелов
return issuerDN.ToString().Trim();
}
4. Рассмотрите возможность использования X509Principal
Для более сложной обработки издателя рассмотрите возможность использования X509Principal:
X509Principal issuerPrincipal = cert.IssuerX509Principal;
string issuerRfc2253 = issuerPrincipal.ToString();
Примеры кода для правильного сравнения
Полный метод проверки
Вот надежный метод для проверки имен издателей XAdES:
public bool ValidateXAdESIssuerName(X509Certificate signatureCert, string issuerNameXml)
{
try
{
// Вариант 1: Прямое строковое сравнение (менее надежно)
X509Name xmlIssuerDN = new X509Name(issuerNameXml);
bool equivalent = signatureCert.IssuerDN.Equivalent(xmlIssuerDN);
// Вариант 2: Сравнение на основе сертификата (более надежно)
X509Certificate xmlCert = new X509Certificate(Convert.FromBase64String(keyInfoXml.InnerText));
bool certMatches = signatureCert.IssuerDN.Equivalent(xmlCert.IssuerDN) &&
signatureCert.SerialNumber.Equals(xmlCert.SerialNumber);
return certMatches; // Предпочитаем сравнение на основе сертификата
}
catch (Exception ex)
{
// Логируем ошибку и обрабатываем соответствующим образом
return false;
}
}
Создание подписи XAdES с правильной обработкой издателя
public string CreateXAdESSignature(X509Certificate signingCert, byte[] dataToSign)
{
// Получаем исходные байты DN издателя для XML
byte[] issuerBytes = signingCert.GetIssuerX509Principal().GetEncoded();
string issuerName = new X509Name(issuerBytes).ToString();
// Создаем XML структуру с последовательным форматированием имени издателя
string issuerXml = $"<ds:X509IssuerName>{EscapeXml(issuerName)}</ds:X509IssuerName>";
// Продолжаем создание подписи XAdES...
return CreateSignatureXml(signingCert, dataToSign, issuerXml);
}
private string EscapeXml(string input)
{
return input.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
Заключение
Расхождение в кодировках DER между эквивалентными объектами X509Name является нормальным и ожидаемым поведением в Bouncy Castle. Это происходит из-за различий между:
- Исходной кодировкой DER - сохраняется при извлечении непосредственно из сертификатов
- Реконструкцией на основе строки - вносит вариации в форматирование и упорядочивание
Для проверки подписей XAdES вы должны:
- Предпочитать логическую эквивалентность с использованием метода Equivalent() вместо сравнения на уровне байтов
- Использовать сравнение на основе сертификата, когда это возможно, сравнивая и издателя, и серийный номер
- Нормализовать последовательно, если необходимо преобразовывать между строковыми и DER-форматами
- Сохранять исходные кодировки при работе с XML-структурами XAdES
Ключевое понимание заключается в том, что имена X.509 семантически эквивалентны даже при их различных кодировках DER, что объясняет, почему спецификация X.509 и реализация Bouncy Castle фокусируются на логической эквивалентности, а не на строгом побайтовом совпадении.
Источники
- Документация Bouncy Castle C# - Класс X509Name
- RFC 4514 - Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names
- RFC 5280 - Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile
- Спецификация XAdES 1.3.2 - ETSI TS 101 903 V1.3.2
- Исходный код Bouncy Castle - Реализация X509Name