💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
digital-signing-certificate-validity-dotnet
감사인이 확인하기 전까지 보이지 않는 컴플라이언스 문제
서명 서비스가 3년 동안 오류 없이 운영됩니다. 문서는 발송되고, 수신자는 이를 수락하며, 로그에는 문제가 없다고 보입니다. 그런데 거래 상대방의 검증자가 한 배치를 유효하지 않다고 표시하고, 조사 결과 두 가지 원인이 발견됩니다: 서명이 SHA‑1로 작성되었고, 지난 4개월 동안 인증서가 만료된 상태였습니다.
두 실패 모두 서명 시점에서는 조용히 발생했습니다. 이것이 바로 GroupDocs.Signature 26.9가 바꾸는 점입니다.
인증서 유효성 강제 적용은 .NET 디지털 서명의 새로운 기본 동작입니다: 유효 기간을 벗어난 인증서는 사용되지 않고 거부됩니다. 이와 함께 두 가지 동반 기능이 제공됩니다 — 기본 PDF 다이제스트는 SHA‑256이며, 최종적으로 필터링을 수행하는 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 디지털 서명은 이제 현재 검증자가 기대하는 adbe.pkcs7.detached 형식으로 SHA‑256을 사용해 작성됩니다. 이전 버전은 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에 실패하고 누군가가 페이지를 받게 됩니다. 이는 실제 비용이며, 여전히 더 작은 비용입니다. 거부된 배치는 하나의 알림, 하나의 갱신, 하나의 재실행으로 여러분의 시스템 안에서 해결됩니다. 만료된 인증서로 서명된 배치는 수신자가 발견하게 되며, 이는 지원 티켓, 모든 영향을 받은 문서의 재발행, 그리고 얼마나 오래 지속됐는지에 대한 어색한 대화를 초래합니다.
샘플은 실패를 이론이 아닌 구체적인 예제로 보여줍니다: 의도적으로 만료된 인증서로 서명하고 예외를 잡아 메시지를 출력하므로, 업그레이드가 프로덕션에 적용되기 전에 로그에 어떤 내용이 들어갈지 정확히 확인할 수 있습니다. 버전 업그레이드 일정을 잡기 전에 자체 인증서 저장소에 대해 이 메서드를 실행해 보시기 바랍니다.
업그레이드 전 해야 할 일
가능성이 높은 순서대로 세 가지 점검 항목이 있습니다.
- 모든 서명 경로에서 인증서 만료일을 확인하세요. 특히 월간·분기별로 실행되는 경로가 가장 오래 만료된 인증서를 숨깁니다.
HashAlgorithm을 검색하세요. 설정이 없으면 업그레이드 시 다이제스트가 SHA‑1에서 SHA‑256으로 바뀌며, 이는 릴리즈 노트에 포함될 개선 사항입니다.- 로그 레벨을 의도적으로 결정하세요. 서비스의 정직한 기본값은
Warning | Error이며,All은 특정 문제를 재현할 때,None은 서명이 면제된 상황을 알리는 유일한 신호를 포기하는 것입니다.
검증도 같은 방향으로 변경되었습니다
놓치기 쉬운 부분입니다. 호출 코드에서 별다른 변경이 필요하지 않기 때문입니다. 기준이 없는 DigitalVerifyOptions는 이전에 거의 동작하지 않았지만, 26.9부터는 모든 PDF 디지털 서명에 대해 전체 암호학적 검사를 수행합니다.
수신 문서를 검증하는 서비스라면, “여기에 서명이 있다”에서 “이 서명이 이 내용과 일치한다”로 조용히 업그레이드된 것입니다. 지난 달에 검증을 통과했던 문서가 이제 실패한다면, 문서가 변경되었고 이전 검증은 이를 확인하지 못했음을 의미합니다.
샘플에 포함된 인증서
코드보다 복사할 가치가 있는 한 가지 세부 사항: 샘플에는 개인 키가 포함되지 않습니다. TestCertificates.cs는 실행 시 메모리에서 세 개의 자체 서명 PFX를 생성합니다 — 현재 유효, 작년 만료, 내년부터 유효 — 따라서 오늘 날짜와 관계없이 시연이 가능하고, 저장소에 민감한 정보가 없습니다.
이 패턴은 자체 테스트 스위트에 적용할 가치가 있습니다. 커밋된 테스트 인증서는 결국 만료되며, 만료 시 실패는 이번 릴리즈가 표면화하도록 만든 버그와 정확히 동일하게 나타납니다.
결론
세 가지 변경, 하나의 방향: 수신자에게 나타나던 실패가 이제 발신자에게 나타납니다. AllowExpired 대신 인증서를 갱신하고, 기본값을 SHA‑256으로 두며, 들어오는 문서를 암호학적으로 검증하고, 필요하기 전에 로그 레벨을 선택하세요. 샘플은 거부를 포함한 여섯 가지 동작을 한 번에 실행하므로 몇 분 안에 업그레이드를 연습할 수 있습니다.