💡 Pełny działający przykład dostępny na GitHubie:
pdf-signing-certificate-checks-python
Wprowadzenie
Usługa co noc podpisuje przesłane pliki PDF. Pewnego ranka używany certyfikat przekroczył datę ważności i nic się nie zmieniło: zadanie uruchomiło się, pliki zostały zapisane, a dziennik wyglądał normalnie. Kilka tygodni później ktoś otworzył jeden z tych dokumentów w Acrobat i zobaczył baner ostrzegawczy, ponieważ podpis wykonany przy użyciu wygasłego certyfikatu nie jest słabszy – jest uznawany przez weryfikatory za nieważny. Dokumenty, które wydawały się zatwierdzone, są warte mniej niż te niepodpisane, ponieważ ludzie w nie wierzyli.
To odrzucenie ma nazwę. Sprawdzanie ważności certyfikatu jest zachowaniem GroupDocs.Signature dla Pythona, które odmawia podpisu, gdy okres ważności certyfikatu upłynął lub jeszcze nie rozpoczął się. Zostało wprowadzone w wersji 26.9 wraz z dwoma zmianami o tym samym charakterze: SHA‑256 stało się domyślnym skrótem dla podpisów PDF, a SignatureSettings.log_level zaczął filtrować zamiast być po cichu ignorowany. Każda z nich przekształca wynik, który wcześniej zachodził w ciszy, w coś, co jest widoczne.
W tym artykule porównujemy te trzy kontrolki, opisując ich zachowanie w Pythonie poprzez .NET – co każda z nich zmienia w wyniku, kiedy warto z niej skorzystać oraz które dwa szczegóły powiązania kosztowały mnie popołudnie. Wszystkie przytoczone wyniki pochodzą z uruchomienia przykładu na jednopaginowym pliku PDF.
Dlaczego to ważniejsze niż notka o wersji
Trzy zmiany mają wspólną cechę, którą warto nazwać: wszystkie zamieniają błąd, który odkryłbyś później, w błąd, który odkrywasz od razu.
- Wygasłe certyfikaty: wywołanie podpisu kończy się niepowodzeniem, dając możliwość odnowienia certyfikatu, zamiast tworzyć dokumenty, które po dystrybucji nie przejdą weryfikacji
- Domyślne skróty: nowe podpisy używają SHA‑256 bez konieczności ręcznego ustawiania, więc słabsza opcja wymaga świadomej decyzji, a nie nieuwagi
- Poziomy logowania: usługa, która konfiguruje tylko ostrzeżenia, otrzymuje wyłącznie ostrzeżenia, co sprawia, że są one czytelne i w efekcie faktycznie odczytywane
Ostatnia z nich jest mniej kosmetyczna, niż się wydaje. Cała wartość ostrzeżenia o wygasłym certyfikacie polega na tym, że ktoś je zobaczy, a ostrzeżenie ukryte w dziesięciu komunikatach śledzenia na każde uruchomienie jest po prostu niewidoczne.
Wymagania wstępne
Przed rozpoczęciem upewnij się, że masz:
- Python 3.9 lub nowszy na interpreterze 64‑bitowym – pakiet dostarcza własny środowisko uruchomieniowe .NET i nie posiada wersji 32‑bitowej
- GroupDocs.Signature dla Pythona poprzez .NET w wersji 26.10.0, z bezpłatną tymczasową licencją, jeśli chcesz usunąć ograniczenia wersji ewaluacyjnej
- Plik PDF do podpisania oraz pakiet
cryptography, jeśli chcesz generować jednorazowe certyfikaty testowe, tak jak w przykładzie
Instalacja
pip install groupdocs-signature-net cryptography
Kontrolka 1 – Skrót zapisywany w podpisie
hash_algorithm w DigitalSignOptions wybiera skrót. Domyślnie od wersji 26.9 jest to SHA‑256, w formacie adbe.pkcs7.detached, którego oczekują obecni weryfikatorzy; wcześniej nowe podpisy używały SHA‑1.
with signature.Signature(source_path) as sign:
options = DigitalSignOptions()
options.certificate_stream = io.BytesIO(pfx)
options.password = PASSWORD
options.hash_algorithm = HashAlgorithm.SHA512
options.reason = "Approved"
result = sign.sign(output_path, options)
return len(result.succeeded)
Warto wyróżnić dwa szczegóły. Certyfikat jest przekazywany przez certificate_stream jako io.BytesIO, a nie jako ścieżka do pliku – tak PKCS#12 zbudowany w pamięci trafia do biblioteki bez zapisywania na dysku; przykład polega właśnie na tym, aby nie dostarczać żadnego klucza prywatnego. HashAlgorithm oferuje AUTO, SHA1, SHA256, SHA384 i SHA512; znacznik czasu, jeśli go dodasz, użyje tego samego skrótu, którego użyto w podpisie.
W praktyce jest to kontrolka, której najrzadziej dotykasz. Domyślna wartość jest już prawidłowa, SHA384 i SHA512 istnieją na wypadek, gdyby polityka podpisu je wymagała, a SHA1 jest ustawieniem kompatybilności dla weryfikatorów, których nie możesz zmienić.
Kontrolka 2 – Czy przeterminowany certyfikat ma zatrzymać proces
Bez nadpisywania, podpisanie certyfikatem, którego okres ważności już się zakończył – lub jeszcze nie rozpoczął – podnosi GroupDocsSignatureException i nie zapisuje niczego.
try:
sign.sign(output_path, options)
return True
except signature.GroupDocsSignatureException as error:
print(f"Rejected: {str(error).splitlines()[0]}")
return False
Komunikat podaje nazwę certyfikatu, datę wygaśnięcia, odcisk palca oraz właściwość, która pozwoliłaby na jego użycie – to wystarczy, aby aplikacja mogła poinformować operatora, co odnowić. Pobranie tylko pierwszej linii ma znaczenie w Pythonie: tekst wyjątku kontynuuje się śladem stosu .NET po stronie powiązania i nie powinien być wyświetlany użytkownikowi.
Kiedy naprawdę musisz podpisać mimo wszystko – test na archiwalnym certyfikacie lub partia, która musi zostać uruchomiona dziś wieczorem, gdy odnowienie jest w toku – nadpisanie odbywa się przy wywołaniu:
settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR
with signature.Signature(source_path, settings=settings) as sign:
options.allow_expired = True
result = sign.sign(output_path, options)
allow_not_yet_valid ma taki sam kształt dla certyfikatu wydanego na późniejszą datę, a dwa flagi są niezależne: zezwolenie na wygasły certyfikat nie zezwala na certyfikat „przedwcześnie” ważny. Wcześniejszy certyfikat zazwyczaj oznacza, że zegar maszyny jest nieprawidłowy, a nie że sam certyfikat jest nietypowy; nieprawidłowy zegar sprawia, że każdy podpis generowany przez tę maszynę jest wątpliwy, więc sprawdź go przed nadpisaniem czegokolwiek.
Oba nadpisania generują ostrzeżenie zamiast milczącego przejścia, co jest częścią łączącą je z trzecią kontrolką.
Kontrolka 3 – Czy ktoś to zauważy
SignatureSettings.log_level jest wartością flagową. Przykład podpisuje ten sam dokument trzykrotnie, przy LogLevel.NONE, LogLevel.WARNING | LogLevel.ERROR i LogLevel.ALL, licząc, co się pojawia:
logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level
with signature.Signature(source_path, settings=settings) as sign:
options.allow_expired = True
sign.sign(output_path, options)
Wyniki to kolejno: nic, jedno ostrzeżenie, a potem to ostrzeżenie plus dziesięć komunikatów śledzenia. Przed wersją 26.9 wszystkie trzy wiersze byłyby identyczne, ponieważ poziom był akceptowany i ignorowany – warto o tym wiedzieć, jeśli kiedykolwiek ustawiłeś go, nie zauważyłeś zmiany i uznałeś, że źle odczytałeś własny kod.
Dwa szczegóły powiązania kosztowały mnie popołudnie, więc warto je jasno przedstawić. SignatureSettings.logger jest tylko do odczytu, więc logger musi być przekazany jako argument konstruktora, a próba przypisania go później podnosi AttributeError; log_level ustawia się normalnie po konstrukcji. Dodatkowo własny logger nie może dziedziczyć po groupdocs.signature.logging.ILogger – ta klasa bazowa opakowuje natywny obiekt, którego konstruktor wymaga uchwytu zarządzanego przez bibliotekę, więc dziedziczenie podnosi TypeError. Powiązanie akceptuje dowolny zwykły obiekt, który udostępnia trzy metody:
class StdlibLogger:
def error(self, message, exception=None):
logging.getLogger("groupdocs").error(message, exc_info=exception)
def warning(self, message, exception=None):
logging.getLogger("groupdocs").warning(message)
def trace(self, message):
logging.getLogger("groupdocs").debug(message)
error i warning mogą przyjmować opcjonalny parametr exception. Biblioteka nie zawsze go przekazuje, a logger wymagający tego parametru przestaje działać przy komunikatach, które go nie zawierają.
Porównanie trzech: kiedy używać której
| Kontrolka | Najlepsze zastosowanie | Kluczowe zalety | Ograniczenia |
|---|---|---|---|
hash_algorithm |
spełnienie polityki określającej skrót | jednorazowe ustawienie; ten sam rozmiar wyjścia | bez sensu, jeśli sam certyfikat jest niegodny zaufania |
| sprawdzanie ważności i nadpisywanie | każdy, kto podpisuje w imieniu innych | niepowodzenie pojawia się tam, gdzie można je naprawić | nadpisanie tworzy plik, ale nie jest on godny zaufania |
log_level |
usługi, których logi są już zatłoczone | jedenaście komunikatów zamienia się w jeden | filtruje jedynie logowanie, nigdy nie ukrywa wyjątków |
To nie są alternatywy – jedno wywołanie podpisu wykorzystuje wszystkie trzy. Kolejność myślenia powinna odzwierciedlać kolejność konsekwencji: sprawdzenie ważności decyduje, czy plik w ogóle istnieje, skrót decyduje, co znajduje się w środku, a poziom logowania decyduje, kto o tym wie.
Czy poziom logowania zmienia rodzaj otrzymywanych wyjątków?
Nie. Decyduje jedynie, które komunikaty trafią do twojego loggera i nic więcej. Wygasły certyfikat nadal podnosi GroupDocsSignatureException przy LogLevel.NONE, a allow_expired nadal podpisuje przy LogLevel.ALL; wartości zwracane i wyjątki są identyczne we wszystkich poziomach. Zmienia się jedynie to, czy ostrzeżenie wyjaśniające wątpliwy podpis zostanie kiedykolwiek przeczytane przez człowieka.
Weryfikacja przesunięta w tym samym kierunku
Warto o tym wspomnieć, bo jest drugą połową tego samego wydania. verify z pustym DigitalVerifyOptions teraz kryptograficznie sprawdza każdy cyfrowy podpis PDF, więc dokument zmieniony po podpisaniu zostaje uznany za nieważny, a nie jedynie niewyjaśniony:
with signature.Signature(signed_path) as sign:
result = sign.verify(DigitalVerifyOptions())
return result.is_valid
Dwa wiersze kodu, a warto dodać je do każdego potoku, który najpierw podpisuje, a potem przechowuje. Zwróć uwagę, czego True nie obiecuje: mówi, że podpis pasuje do dokumentu, ale nie że wystawca jest zaufany. Samopodpisane certyfikaty użyte w przykładzie przechodzą weryfikację tutaj, a wciąż są odrzucane przez czytnik PDF, co oddzielnie odpowiada na pytanie o zaufanie.
Najlepsze praktyki i wskazówki
- Utrzymuj odrzucenie jako domyślne w każdym rozwiązaniu podpisującym w imieniu użytkowników i nadpisuj je jedynie przy konkretnym wywołaniu, a nie globalnie. Wyjątek jest tani; partia nieprawidłowych podpisów nie jest.
- Loguj treść ostrzeżenia, a nie tylko licznik. Zawiera ona nazwę certyfikatu i datę, co jest jedyną częścią, na którą operator może zareagować.
- Sprawdź zegar przed dopuszczeniem certyfikatu, który jeszcze nie jest ważny. Zazwyczaj certyfikat jest prawidłowy, a maszyna nie, co wpływa na więcej niż jedno wywołanie podpisu.
- Trzymaj trace’y z dala od produkcji. Około dziesięciu na każde uruchomienie szybko się sumuje; włącz je podczas diagnozowania, a wyłącz po zakończeniu.
- Weryfikuj po podpisaniu w każdym potoku, teraz gdy sprawdzenie jest kryptograficzne, aby uszkodzony wynik został wykryty zanim odbiorca go znajdzie.
Podsumowanie
Trzy kontrolki, jedno wywołanie podpisu i ta sama koncepcja projektowa stojąca za wszystkimi: ryzykowny wynik wymaga teraz decyzji, a bezpieczny nie wymaga niczego. Zachowaj sprawdzanie ważności, traktuj allow_expired jako wyjątek per‑wywołanie, który logujesz, pozostaw skrót w spokoju, chyba że polityka wymaga inaczej, i ustaw poziom logowania tak, aby ostrzeżenia były czytelne.
Uruchomienie przykładu na własnym pliku PDF zajmuje minutę i wypisuje dokładnie to, co zmieniła każda kontrolka – sześć podpisanych plików, jedno celowe odrzucenie i trzy wiersze liczników komunikatów, które już nie wyglądają tak samo.