Другое

Эквивалентные 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

Проблема, с которой вы столкнулись, на самом деле довольно распространена при обработке сертификатов X.509. Когда вы получаете имя издателя из сертификата с помощью signingCert.IssuerDN.ToString(), Bouncy Castle создает строковое представление distinguished name (DN). Эта строка обычно следует формату RFC 4514, который является удобочитаемым и стандартизированным для операций с каталогами.

Однако при разборе этой строки обратно в объект X509Name с помощью new X509Name(_issuerSerial.X509IssuerName) процесс разбора может интерпретировать и закодировать имя иначе, чем исходный DN издателя сертификата. Это происходит потому, что:

  1. Исходный DN издателя сертификата был закодирован непосредственно из его DER-структуры
  2. Строковое представление проходит через процесс кодирования → декодирования → повторного кодирования
  3. Строковые представления часто нормализуют или переформатируют компоненты имен

Согласно документации 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. Кодирование строковых значений

Различные подходы к кодированию строк могут вызывать различия на уровне байтов:

csharp
// Пример: различия в кодировании Unicode vs ASCII
// Unicode: UTF-16 с BOM или явная кодировка
// Кодировка DER: UTF-8 для большинства строковых атрибутов

4. Вариации кодирования BER vs DER

Хотя DER должен быть строгой канонической кодировкой, некоторые реализации могут создавать слегка разные кодировки для одной и той же логической структуры из-за:

  • Различных подходов к кодированию длины
  • Обработки необязательных элементов
  • Вариаций номеров тегов

Сравнение методов создания X509Name

Рассмотрим два разных пути, которые приводят к вашему сравнению:

Путь 1: Из сертификата (исходный DER)

csharp
X509Certificate xmldsigCert = new X509Certificate(Convert.FromBase64String(_keyInfoXml.InnerText));
X509Name dsigIssuerDN = dsigCert.IssuerDN;
Byte[] dsigIssuerBytes = dsigIssuerDN.GetEncoded();

Этот путь:

  • Читает сертификат непосредственно из DER-закодированных байтов
  • Извлекает DN издателя так, как он был изначально закодирован
  • Сохраняет точную байтовую последовательность из сертификата

Путь 2: Из строки (реконструированный)

csharp
X509Name signingCertIssuerDN = new X509Name(_issuerSerial.X509IssuerName);
Byte[] signingCertIssuerBytes = signingCertIssuerDN.GetEncoded();

Этот путь:

  • Принимает удобочитаемое строковое представление
  • Разбирает и реконструирует объект X509Name
  • Повторно кодирует его в формат DER, потенциально с различиями

Ключевое отличие заключается в том, что первый путь сохраняет исходную кодировку, тогда как второй путь проходит через промежуточный удобочитаемый формат, который вносит вариации.


Практические решения и обходные пути

Решение 1: Использование исходных байтов сертификата для сравнения

Вместо сравнения реконструированных имен издателей, сравнивайте сертификаты непосредственно:

csharp
// Извлекаем исходные байты DN издателя из обоих сертификатов
byte[] cert1IssuerBytes = cert1.GetIssuerX509Principal().GetEncoded();
byte[] cert2IssuerBytes = cert2.GetIssuerX509Principal().GetEncoded();

// Сравниваем исходные кодировки
bool isSameIssuer = cert1IssuerBytes.SequenceEqual(cert2IssuerBytes);

Решение 2: Нормализация перед сравнением

Создайте функцию нормализации, обеспечивающую последовательное кодирование:

csharp
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 вы можете сравнивать весь сертификат, а не только имя издателя:

csharp
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() достаточен и более надежен, чем сравнение на уровне байтов:

csharp
bool isValidIssuer = dsigIssuerDN.Equivalent(signingCertIssuerDN);

3. Обрабатывайте строковые представления с осторожностью

Если вы должны использовать строковые представления для вывода XML:

csharp
// Используйте последовательный метод форматирования
string issuerNameXml = FormatIssuerNameForXml(cert.IssuerDN);

private string FormatIssuerNameForXml(X509Name issuerDN)
{
    // Используйте последовательное форматирование без лишних пробелов
    return issuerDN.ToString().Trim();
}

4. Рассмотрите возможность использования X509Principal

Для более сложной обработки издателя рассмотрите возможность использования X509Principal:

csharp
X509Principal issuerPrincipal = cert.IssuerX509Principal;
string issuerRfc2253 = issuerPrincipal.ToString();

Примеры кода для правильного сравнения

Полный метод проверки

Вот надежный метод для проверки имен издателей XAdES:

csharp
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 с правильной обработкой издателя

csharp
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("&", "&amp;")
                .Replace("<", "&lt;")
                .Replace(">", "&gt;")
                .Replace("\"", "&quot;")
                .Replace("'", "&apos;");
}

Заключение

Расхождение в кодировках DER между эквивалентными объектами X509Name является нормальным и ожидаемым поведением в Bouncy Castle. Это происходит из-за различий между:

  1. Исходной кодировкой DER - сохраняется при извлечении непосредственно из сертификатов
  2. Реконструкцией на основе строки - вносит вариации в форматирование и упорядочивание

Для проверки подписей XAdES вы должны:

  • Предпочитать логическую эквивалентность с использованием метода Equivalent() вместо сравнения на уровне байтов
  • Использовать сравнение на основе сертификата, когда это возможно, сравнивая и издателя, и серийный номер
  • Нормализовать последовательно, если необходимо преобразовывать между строковыми и DER-форматами
  • Сохранять исходные кодировки при работе с XML-структурами XAdES

Ключевое понимание заключается в том, что имена X.509 семантически эквивалентны даже при их различных кодировках DER, что объясняет, почему спецификация X.509 и реализация Bouncy Castle фокусируются на логической эквивалентности, а не на строгом побайтовом совпадении.

Источники

  1. Документация Bouncy Castle C# - Класс X509Name
  2. RFC 4514 - Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names
  3. RFC 5280 - Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile
  4. Спецификация XAdES 1.3.2 - ETSI TS 101 903 V1.3.2
  5. Исходный код Bouncy Castle - Реализация X509Name
Авторы
Проверено модерацией
Модерация