İmzalı bir fatura bayt bayt doğrulanır, imzalayanı geçen ay iptal edilmiştir ve getirdiğiniz CRL seri numarasını hiç listemez: o CRL başka bir bölümden söz eder. HotPDF'teki yol doğrulama tam da bu tür durumlar için tasarlanmıştır: RFC 5280 kapsam ve çıpa kuralları, UTC üzerinden işleyen bir değerlendirme saati ve TSA zinciri güvenilir oluncaya kadar geçerli olmayan RFC 3161 zaman damgaları
Değerlendirme saati UTC ile çalışır
Bir yol doğrulayıcının karşılaştırdığı her an UTC'de saklanır. RFC 5280 §4.1.2.5, sertifika geçerlilik tarihlerinin Greenwich Mean Time ile UTCTime ya da GeneralizedTime olarak kodlanmasını ister; CRL thisUpdate ve nextUpdate aynı kodlamaları kullanır ve RFC 6960 OCSP yanıtları producedAt, thisUpdate ve nextUpdate alanlarını UTC cinsinden GeneralizedTime olarak taşır. Delphi'in TDateTime türünün zaman dilimi yoktur ve Now yerel saat duvarını döndürür; if Now > NotAfter diye yazılmış bir denetim, derleyici uyarısı olmadan iki saati harmanlar. Hata coğrafyaya göre ikiye ayrılır. Greenwich'in batısında erken çalışır. New York'taki bir sunucuda yerel saat 09:00'ı gösterirken dünya saati 14:00Z okur; 13:59Z'de üretilmiş bir OCSP yanıtı o saf saate göre beş saat gelecededir ve HotPDF'in varsayılan beş dakikalık tolerans penceresi OCSPAllowedClockSkewMinutes'e burun buruna gelmek bir yana, henüz geçerli değil diye reddedilir. Şanghay'da işaret tersine döner: yerel saat 22:00, yani 14:00Z iken, notAfter değeri 20:00Z olan bir imzalama sertifikası sekiz saat önce çoktan süresi dolmuş görünür. İki başarısızlık da UTC'ye ayarlı bir build agent'ta görünmez — barındırılan çoğu CI runner'ın varsayılanı budur
HotPDF, THPDFCertificateValidationOptions.ValidationTime alanını UTC olarak ele alır. Varsayılanı olan 0'da bırakılırsa değerlendirme zamanı, güvenilir bir zaman damgası imzalama zamanını vermiyorsa (aşağıda değineceğiz), geçerli UTC zamanı olur ve gerçekte kullanılan an Info.CertificateValidation.EvaluationTime ile geri döner. VerifyLoadedSignatureWithTrust, bunların hiçbirinden önce svValid arkasındaki bayt aralığı doğrulamasını koşturur, sonra yolu kurar. Tarihsel bir anı kendiniz sabitleyecekseniz — diyelim ki çeyrek kapanışında bir fatura imzasının iyi olup olmadığını soran bir denetçi var — dönüşüm sizin işiniz. System.DateUtils'teki TTimeZone.Local.ToUniversalTime bunu yapar. Onu, doğrulama çağrısında değil, zaman damgası sisteminize girdiği yerde uygulayın: yazı saati uygulaması olan bölgelerde bir yerel saat her sonbaharda bir kez daha yaşar, bir diğeri de ilkbaharda hiç var olmaz; dolayısıyla yerel zaman damgalarından oluşan bir veritabanı sütunu her anı sağlam geri dönüştüremez
uses
System.SysUtils, System.DateUtils, System.IOUtils,
HPDFDoc, HPDFSignVerify;
var
Pdf: THotPDF;
Options: THPDFCertificateValidationOptions;
Info: THPDFSignatureInfo;
Status: THPDFCertificateValidationStatus;
ClosingLocal: TDateTime;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('invoices-2026-q1.pdf');
Options := THPDFCertificateValidationOptions.Default;
SetLength(Options.TrustAnchors, 1);
Options.TrustAnchors[0] := TFile.ReadAllBytes('issuing-root.cer');
Options.RevocationMode := rvmRequire;
// Çeyrek kapanışı, ERP'nin kaydettiği haliyle, sunucunun yerel diliminde
ClosingLocal := EncodeDateTime(2026, 3, 31, 23, 59, 59, 0);
// Sertifikalar, CRL'ler ve OCSP yanıtları UTC zamanları taşır
Options.ValidationTime := TTimeZone.Local.ToUniversalTime(ClosingLocal);
Status := Pdf.VerifyLoadedSignatureWithTrust(0, Options, Info);
Writeln('Evaluated at (UTC): ',
DateTimeToStr(Info.CertificateValidation.EvaluationTime));
case Status of
cvsTrusted:
Writeln('Trusted via ',
string(Info.CertificateValidation.TrustAnchorSubjectDN));
cvsRevoked:
Writeln('Revoked at (UTC): ',
DateTimeToStr(Info.CertificateValidation.RevokedAt));
else
Writeln('Not trusted: ', string(Info.CertificateValidation.Issue));
end;
finally
Pdf.Free;
end;
end;
Bir CRL aslında hangi sertifikaları kapsar?
Bir CRL yalnızca kendi kapsamındaki sertifikalar adına konuşur ve kapsam, RFC 5280 §5.2.5'in Issuing Distribution Point uzantısında bildirilir. İptal verisini bölümlendiren bir CA birkaç CRL yayımlar; hepsi aynı anahtarla imzalıdır ve hepsi tazedir. Bir sertifikanın bunlardan birinde olmaması, o CRL kendisini kapsadığını iddia etmedikçe hiçbir şey ifade etmez. Kapsamı daralan alanlar şunlardır:
distributionPoint: CRL, yalnızca CRL Distribution Points uzantısı bu noktayı adlandıran sertifikaları kapsaronlyContainsUserCertsveonlyContainsCACerts: yalnızca uç varlık (end-entity) sertifikaları ya da yalnızca CA sertifikalarıonlySomeReasons: bir alt neden kodları kümesi için iptaller; keyCompromise gibionlyContainsAttributeCerts: CRL attribute sertifikalarıyla ilgilidir ve açık anahtar sertifikaları hakkında hiçbir şey söylemezindirectCRL: girişler, CRL'yi imzalayanın dışında ihraç edenlere ait olabilir
Saf arama şöyle işler: CRL imzasını doğrular, thisUpdate ile nextUpdate'i kontrol eder, seri numarasını arar ve numara yoksa sertifikayı iyi ilan eder. O son adım, ancak RFC 5280 §6.3.3 (b)(2)'nin kapsam testinden sonra geçerlidir. 7 numaralı bölümün CRL'si, 3. bölümden bir sertifikayı asla listelemez. Yalnızca CA sertifikalarına kısıtlanmış bir CRL, uç varlık imzalayanını asla listelemez. İkisi de iptal edilmiş bir sertifikaya "bulunamadı" der. HotPDF bir sertifika için ancak şu koşullarda bir temel CRL'ye başvurur: CRL ne attribute sertifikalarına ne birtakım nedenlere kısıtlanmıştır, kullanıcı ya da CA kısıtlaması sertifikayla uyuşur ve adlandırdığı dağıtım noktası, sertifikanın kendi CRL Distribution Points uzantısındaki URI'lerden biriyle — büyük/küçük harfe duyarsız karşılaştırmayla — uyuşmaktadır; CRL ihraç edenine göre görece bir ad olarak verilmiş bir nokta hiçbir şeyle uyuşmaz. Nedenlere kısıtlanmış bir CRL'ye hiç başvurulmaz; dolayısıyla o listelediği iptaller de sayılmaz. §6.3.3, neden bölümlerinden tam kapsam derlemesine izin verir — her neden hesap edilene dek bir neden maskesi izlenir — ama belge imzaları için bir OCSP yanıtı ya da tam kapsamlı bir CRL daha basit kanıttır: bir tanesini toplayın. Kapsam içi kanıt kalmadığında iptal durumu bilinmez ve rvmRequire altında çağrı cvsRevocationUnknown döndürür
Bir yolu kapatan güven çıpaları CA olmak zorundadır
RFC 5280 §6.1.1, yol doğrulama algoritmasına güven çıpasını düz bir bilgi olarak verir: bir ihraç eden adı ve bir açık anahtar. Algoritma, çıpanın kendi sertifikasının bir şey ihraç edip edemeyeceğini asla sormaz; §6.1.4'teki basicConstraints testi yolun ara sertifikalarında çalışır. Çıpalar özenle derlenmiş bir mağazadan gelen kök CA'lar olduğunda bu sorun değildir. Belge iş akışları bu varsayımı bozar, çünkü insanlar uç varlık sertifikalarını sabitler: üç bilinen tedarikçiden sözleşme alan bir satın alma sistemi, çoğu zaman her tedarikçinin imza sertifikasına doğrudan güvenir. Sabitleme, imzalayanla çıpanın aynı sertifika olduğu tek sertifikalı bir yol için çalışır. Tehlike iki sertifikalı yoldadır. Bir tedarikçinin özel anahtarı, istediği herhangi bir konuyu taşıyan taze bir sertifikayı imzalayabilir — mesela CFO'nuzun adını — ve her çıpa anahtarını ihraç anahtarı sayan bir doğrulayıcı, o sahtelikten sabitlenmiş yaprağa kadar tertemiz bir yol kurar
HotPDF, yol tek sertifikadan uzun olduğunda RFC 5280 ihraç eden gereksinimlerini çıpaya da uygular. Bu durumda çıpa, cA=TRUE ile basicConstraints ister (§4.2.1.9); bir keyUsage uzantısı taşıyorsa keyCertSign biti kurulu olmalıdır (§4.2.1.3). basicConstraints uzantısı olmayan bir kök, yalnızca kendinden ihraçlıysa — konu ve ihraç eden adları eşitse — ihraç eden sayılır; bu, uzantılardan önceki eski sürüm 1 köklerini çalışır durumda tutar. Sabitleme de çalışmaya devam eder. Sabitlenmiş bir uç varlık sertifikası kendi imzalarını doğrulamaya devam ederken, onun imzaladığı bir sertifika cvsPathValidationFailed ile geri gelir; Issue string'i, yolu ihraç eden çıpanın bir sertifika makamı olmadığını söyler
Hangi zaman damgaları değerlendirme zamanını belirleyebilir?
Yalnızca güvenilir olanlar. CMS'e gömülü bir RFC 3161 token'i, imza değerinin genTime anında var olduğunu iddia eder ve doğrulayıcılar imzalayan sertifikasını o anda yargılar — bir imzanın sertifikasından uzun yaşamayı sağlayan budur. Token aynı zamanda sadece bir CMS imzası daha. Kendisinin taşıdığı sertifikaya karşı doğrulamak iç tutarlılığı kanıtlar; onu kimin ürettiği hakkında hiçbir şey kanıtlamaz: süresi dolmuş ya da iptal edilmiş bir imzalama anahtarına sahip herkes kendi zaman damgası hizmetini çalıştırabilir, yeni bir imzayı geçen yılın genTime'ıyla damgalayıp sertifikanın geçerlilik aralığına doğru geri yürütebilir. RFC 3161 §2.3, TSA sertifikasının id-kp-timeStamping genişletilmiş anahtar kullanımını tek amacı olarak — critical işaretli — taşımasını ister. HotPDF, Info.Timestamp.GenerationTime'ı ancak token sertifikası, zaman damgalama amacı için, güncel UTC zamanında, sizin çıpalara göre doğrulandıktan sonra değerlendirme zamanı olarak kabul eder. Bir belge zaman damgası, /SubFilter ETSI.RFC3161, ayrı bir durumdur: orada token, doğrulanan imzanın kendisidir ve genTime kendi yoluna uygulanır
Token kendi seçeneklerinizle denetlendiği için TSA kökü, imzalayan köküyle aynı TrustAnchors listesinde durmak zorundadır ve rvmRequire altında TSA zinciri kendi iptal kanıtını taşımak zorundadır. İkisinden birini kaçırın, değerlendirme güncel zamana geri düşer. TSA sertifikasının ömrü diğer sınırdır: token güncel zamanda yargılanır; o sertifika süresi dolduğunda imzalama zamanı değerlendirmesi de onunla gider ve arşivler o noktadan önce bir belge zaman damgası ekler — PAdES LTV kanıtı ve B-LTA zaman damgası yenilemesinin kapsadığı adım budur. Ham CMS baytlarını elinde tutan kod aynı soruyu HPDFCMSTimestampTimeIsTrusted(CMSDER, Options) üzerinden sorabilir; ValidateLoadedSignatureBatch ise bunu bir partideki her imzaya uygular. Aşağıdaki fonksiyon, hangi karara hangi saatin varıldığını raporlar
// Beklenen: yalnızca SignerRoot ile, sertifikası 2025'te dolmuş bir imza,
// güncel UTC zamanında değerlendirilir ve zaman damgası token'i doğrulansa
// bile cvsTrusted geri dönmez; TsaRoot eklendiğinde aynı çağrı genTime'da
// değerlendirilir
function DescribeVerdictClock(Pdf: THotPDF; SigIndex: Integer;
const SignerRoot, TsaRoot: TBytes): string;
var
Options: THPDFCertificateValidationOptions;
Info: THPDFSignatureInfo;
Status: THPDFCertificateValidationStatus;
Clock: string;
begin
Options := THPDFCertificateValidationOptions.Default;
// TSA zinciri, imzalayanla aynı çıpa listesine karşı yargılanır
SetLength(Options.TrustAnchors, 2);
Options.TrustAnchors[0] := SignerRoot;
Options.TrustAnchors[1] := TsaRoot;
Options.RevocationMode := rvmBestEffort;
Options.ValidationTime := 0; // güvenilir bir zaman damgası zamanı verebilir
Status := Pdf.VerifyLoadedSignatureWithTrust(SigIndex, Options, Info);
if Info.Timestamp.Kind <> tskSignature then
Clock := 'no signature timestamp, current UTC time'
else if (Info.Timestamp.Status = svValid) and
(Info.CertificateValidation.EvaluationTime =
Info.Timestamp.GenerationTime) then
Clock := 'trusted timestamp genTime'
else
Clock := 'timestamp not trusted, current UTC time';
Result := Format('%s at %s (UTC), status %d', [Clock,
DateTimeToStr(Info.CertificateValidation.EvaluationTime), Ord(Status)]);
end;
İlk güven kararından önce düşmanca girdi
Doğrulama işinin bir kısmı, hiçbir şey güvenilmeden önce, bir saldırganın tamamen kontrol ettiği veri üzerinde çalışır. Eksik ara sertifikalar, RFC 5280 §4.2.2.1'in Authority Information Access caIssuers URL'lerinden getirilir ve getirilen her ara sertifika sırayla kendi ihraç edeninin URL'sini reklam yapabilir; böylece iş listesi, üzerinde yürürken büyür. Delphi'de bu, bir for döngüsünü eler. Bir for deyiminin son değeri, döngü başlamadan bir kez değerlendirilir — yürüyüş sırasında eklenen URL'ler hiç ziyaret edilmez ve iki seviye eksik bir zincir eksik kalır. Bunun yerine her turda Count'u yeniden okuyan bir while döngüsü kullanın. HotPDF'de MaxFetch, HPDFFetchAIAIntermediates üzerinde bir yürüyüşün kaç sertifika indireceğini sınırlar; MaxFetchedIntermediates ise aynı sınırı, yol doğrulamasının kendi başına getirmesini sağlayan FetchMissingIntermediates için ayarlar
Retry-After ipucu da yabancının seçtiği bir değerdir. Onun sunucusu, doğrulanmamış bir sertifika tarafından adlandırıldı ve her istekte bir saatlik duraklama isteyebilir. Sınırını çizin. THPDFSignatureEvidenceRetryPolicy.MaxRetryAfterMS, HotPDF'in kanıt ve AIA getirmeleri için o tavanı koyar; varsayılanı 30,000 ms'dir. Aşağıdaki rutin iki kuralı da uygular — AIA'yı kendi başına tarayan, mesela çevrimdışı bir imzalama katmanının ara sertifika önbelleğini ısıtmak isteyen ekipler için
uses
System.SysUtils, System.Generics.Collections;
type
// Sizin taşıma katmanınız: bir caIssuers hedefini indirir ve aldığı
// sertifikanın reklam yaptığı caIssuers URL'lerini döndürür
TFetchIssuer = reference to function(const URL: string;
out NextURLs: TArray<string>; out RetryAfterMS: Cardinal): Boolean;
function WalkCAIssuers(const SeedURLs: TArray<string>;
const Fetch: TFetchIssuer; MaxFetch: Integer;
MaxRetryAfterMS: Cardinal): Integer;
var
Queue: TList<string>;
Seen: TDictionary<string, Boolean>;
NextURLs: TArray<string>;
URL: string;
I, Attempt: Integer;
RetryAfter: Cardinal;
Ok: Boolean;
procedure Enqueue(const AURL: string);
begin
if not Seen.ContainsKey(LowerCase(AURL)) then
begin
Seen.Add(LowerCase(AURL), True);
Queue.Add(AURL);
end;
end;
begin
Result := 0;
Queue := TList<string>.Create;
Seen := TDictionary<string, Boolean>.Create;
try
for URL in SeedURLs do
Enqueue(URL);
I := 0;
// Bir for döngüsü Queue.Count'u girişte sabitler ve aşağıda getirilen
// sertifikaların eklediği URL'lere asla ulaşmaz
while (I < Queue.Count) and (Result < MaxFetch) do
begin
Ok := False;
for Attempt := 1 to 3 do
begin
NextURLs := nil;
RetryAfter := 0;
Ok := Fetch(Queue[I], NextURLs, RetryAfter);
if Ok or (Attempt = 3) then
Break;
// Bekleme sunucunun isteğidir ve sunucuyu doğrulanmamış bir
// sertifika seçmiştir
if RetryAfter > MaxRetryAfterMS then
RetryAfter := MaxRetryAfterMS;
Sleep(RetryAfter);
end;
Inc(I);
Inc(Result);
if Ok then
for URL in NextURLs do
Enqueue(URL);
end;
finally
Seen.Free;
Queue.Free;
end;
end;
CMS imza denetiminin kendisi yol doğrulamasından önce çalışır; doğrulanmayan bir imza için yol kurmak boşa emektir ve denetim, imzada gömülü sertifikanın açık anahtarını kullanır. Bu sıralama, en pahalı aritmetiğin koştuğu anda anahtar boyutunu saldırgana bırakır. Henüz hiçbir şey güvenilmez değildir. RSA doğrulama maliyeti hem modül hem üs uzunluğuyla büyür; devasa bir modüle ve az kalsın ona eşit uzunlukta bir üsse sahip bir sertifika, yüklenen dosyaları doğrulayan bir sunucuda bir iş parçacığını modüler üstel almda oyalayabilir. HotPDF 16,384 bitin üstünde bir RSA modülünü reddeder, modülden uzun bir üssü reddeder ve 3,072 bitin üstünde yalnızca en fazla 64 bitlik üsleri kabul eder. Gerçek imzalama anahtarları e = 65537 ile — 17 bitlik bir üs — 2,048 ile 4,096 bit arasındadır; tavan onlara hiç dokunmaz. Taban ayrı, yapılandırılabilir bir politikadır: AlgorithmPolicy.MinimumRSAKeyBits, varsayılanı 2,048'dir
Aynı imza, zaman damgasının genTime'ında cvsTrusted olabilir ve bugünün tarihinde düşebilir. HotPDF Delphi PDF component döndüren her kararın yanına Info.CertificateValidation.EvaluationTime ile TrustAnchorSubjectDN'yi log'layın; kaydı sonra okuyan bir denetçi, cevabın hangi saate ve hangi çıpaya dayandığını görsün