💡 Полный рабочий пример доступен на GitHub:
digital-signing-certificate-validity-dotnet
Проблема соответствия, которую никто не видит, пока не появится аудитор
Сервис подписи работает три года без ошибок. Документы отправляются, получатели их принимают, в журналах нет никаких признаков проблемы. Затем валидатор контрагента помечает партию как недействительную, и расследование выявляет две причины: подписи были созданы с использованием SHA‑1, а в течение последних четырёх месяцев сертификат был просрочен.
Обе ошибки были тихими в момент подписи. Именно это меняет GroupDocs.Signature 26.9.
Принудительная проверка срока действия сертификата становится новым поведением по умолчанию для цифровой подписи в .NET: сертификат, находящийся за пределами своего периода действия, отклоняется, а не используется. Он поставляется с двумя спутниками — SHA‑256 в качестве алгоритма хеширования PDF по умолчанию и LogLevel, который наконец‑то действительно фильтрует, — и вместе они перемещают три класса сбоев от получателя к отправителю, где их ещё можно исправить.
Почему молчаливый успех — дорогой результат
Подпись необычна тем, что ошибку делает одна сторона, а обнаруживает её другая. Некорректный счёт‑фактура не проходит в вашей системе; недействительная подпись отклоняется в системе получателя через недели, без возможности диагностики, которую вы могли бы прочитать.
Эта асимметрия объясняет, почему «API вернул успех» не является надёжной гарантией. Старые настройки по умолчанию оптимизировались под отсутствие прерываний вызова, а стоимость падала на получателя и, в конечном итоге, на того, кто вынужден был повторно подписать и отправить несколько сотен документов.
Изменение 1: Истёкшие сертификаты отклоняются
Главное изменение. Sign теперь бросает GroupDocsSignatureException, если срок действия сертификата истёк или ещё не начался, и ничего не записывается на диск.
try
{
signature.Sign(outputPath, options);
return true;
}
catch (GroupDocsSignatureException ex)
{
Console.WriteLine($" Rejected: {ex.Message}");
return false;
}
Сообщение указывает сертификат и свойство, которое позволило бы его использовать, так что оператор, читая строку журнала, может сразу принять меры, не открывая документацию. Для конвейера, который обновился до 26.9 и начал падать, это почти всегда причина — правильный ответ — продление, а не подавление.
Когда действительно нужен старый режим, существует одно свойство:
var options = new DigitalSignOptions(certificate)
{
Password = certificatePassword,
AllowExpired = true
};
Документ подписывается, а предупреждение отправляется в логгер. Валидаторы всё равно отклоняют результат, потому что AllowExpired управляет тем, что библиотека допускает, а не тем, насколько «ценен» сертификат. Параметр‑партнёр AllowNotYetValid покрывает другую границу окна и намеренно независим: разрешение просроченного сертификата не подразумевает тихое разрешение сертификата с будущей датой.
Изменение 2: SHA-256 по умолчанию
Цифровые подписи PDF теперь записываются с SHA‑256 в формате adbe.pkcs7.detached, который ожидают современные валидаторы. Ранние версии использовали SHA‑1.
var options = new DigitalSignOptions(certificate)
{
Password = certificatePassword,
HashAlgorithm = HashAlgorithm.Sha256,
Reason = "Approved",
Location = "Head office"
};
Явное указание свойства необходимо только для перехода дальше — Sha384 или Sha512, если политика требует их — или для оставления Sha1 при работе с валидатором, который не поддерживает другие алгоритмы. Временная метка, добавляемая к подписи, использует тот же хеш.
Проверка изменилась в том же релизе и в том же направлении: DigitalVerifyOptions без критериев ранее почти не делала ничего, а теперь выполняет полную криптографическую проверку, поэтому документ, изменённый после подписи, будет отмечен как недействительный.
Изменение 3: LogLevel действительно фильтрует
SignatureSettings давно принимает логгер. До версии 26.9 уровень игнорировался, поэтому каждое сообщение приходило независимо, и большинство сервисов просто отключали логирование, а не утопали в трассах.
В примере разница измеряется подписью одного и того же документа три раза с подсчитывающим логгером:
var levels = new Dictionary<string, LogLevel>
{
["None"] = LogLevel.None,
["Warning | Error"] = LogLevel.Warning | LogLevel.Error,
["All"] = LogLevel.All
};
None не генерирует сообщений, Warning | Error оставляет единственное предупреждение о разрешённом просроченном сертификате, а All добавляет трассировку на каждый шаг. Сам подсчитывающий логгер — точка интеграции для вашего стека:
public void Warning(string message)
{
Warnings++;
WarningMessages.Add(message);
}
Реализуйте эти три метода для Serilog, NLog или Application Insights, и диагностика библиотеки будет попадать туда, куда попадают остальные логи вашего сервиса.
Изменяет ли уровень логирования какие исключения я получаю?
Нет, и стоит явно это отметить, потому что два понятия выглядят связанными. LogLevel фильтрует то, что достигает ILogger. Исключения бросаются в ваш код независимо: просроченный сертификат без AllowExpired всё равно бросит исключение при LogLevel.None, и ваш блок catch будет работать одинаково. Диагностика и управление потоком — отдельные каналы, именно поэтому безопасно запускать продакшн с Warning | Error.
Отказ дешевле, чем кажется
Возражение против «жёсткой остановки» обычно операционное: ночная партия, которая раньше успешно завершалась, теперь падает в 02:00, и кто‑то получает пейдж. Это реальная стоимость, но всё‑равно меньшая. Отклонённая партия — один сигнал, одно продление и один повторный запуск, всё внутри ваших систем. Партия, подписанная просроченным сертификатом, обнаруживается получателем, что приводит к запросу в поддержку, переизданию каждого затронутого документа и неловкому разговору о том, как долго это происходило.
Пример делает сбой конкретным, а не теоретическим: он намеренно подписывает документ просроченным сертификатом, ловит исключение и выводит сообщение, так что вы видите, что именно будет в журналах до того, как обновление попадёт в продакшн. Я бы запустил этот метод против собственного хранилища сертификатов перед планированием bump‑версии.
Что сделать перед обновлением
Три проверки, в порядке убывающей вероятности «укусить».
- Просмотрите сроки действия сертификатов во всех путях подписи, включая те, что запускаются ежемесячно или ежеквартально — именно там просроченный сертификат прячется дольше всего.
- Пройдитесь по коду в поисках
HashAlgorithm: если нигде не задаётся, ваши хеши переключатся с SHA‑1 на SHA‑256 после обновления, что является улучшением, которое всё равно стоит упомянуть в примечании к выпуску. - Осознанно выберите уровень логирования. Честный дефолт для сервиса —
Warning | Error;Allнужен для воспроизведения конкретной проблемы, аNoneозначает отказ от единственного сигнала, который сообщает, что подпись была сделана с «отказом».
Проверка изменена в том же направлении
Легко пропустить, потому что в вызывающем коде ничего менять не нужно. DigitalVerifyOptions без заданных критериев ранее был почти бездействующим: он сравнивал только переданные критерии, а если их не было, то почти ничего не говорил. Начиная с 26.9 тот же вызов выполняет полную криптографическую проверку каждой цифровой подписи PDF.
Для сервиса, проверяющего входящие документы, это тихое обновление от «здесь есть подпись» к «эта подпись соответствует этому содержимому». Полезно знать до того, как вы увидите, как документ, прошедший проверку в прошлом месяце, теперь не проходит: документ, вероятно, был изменён, а старый чек просто этого не учитывал.
Сертификаты в примере
Одна деталь, которую стоит скопировать, а не код: пример не поставляет закрытый ключ. TestCertificates.cs создаёт три самоподписанных PFX‑файла в памяти во время выполнения — действительный, просроченный в прошлом году, действительный с следующего года — так что демонстрация работает независимо от текущей даты и в репозитории нет ничего конфиденциального.
Эту схему стоит принять в собственных тестовых наборах. Закоммиченный тестовый сертификат рано или поздно истечёт, и когда это произойдёт, сбой будет выглядеть точно так же, как баг, который этот релиз призван выявить.
Заключение
Три изменения, одно направление: сбои, которые раньше появлялись у получателя, теперь появляются у отправителя. Продлите сертификат, а не используйте AllowExpired, сделайте SHA‑256 дефолтом, криптографически проверяйте входящие документы и выберите уровень логирования заранее. Пример запускает все шесть поведений за один проход, включая отклонение, так что обновление можно отрепетировать за пару минут.