Το PDFium VCL μπορεί πλέον να ολοκληρώσει μια αλυσίδα υπογραφής PDF και να ελέγξει ανάκληση μέσω δικτύου στο OpenSSL backend του: όταν είναι ενεργοποιημένο το OnlineRetrieval, το ConfigureSslCmsVerifier εγκαθιστά έναν verifier που κατεβάζει λείποντα ενδιάμεσα πιστοποιητικά από τα URLs caIssuers του AIA και CRLs από τα CRL distribution points, μέσα σε σταθερό budget χρόνου, requests και bytes ανά κλήση επαλήθευσης. Τα κατεβασμένα πιστοποιητικά είναι μόνο και μόνο υλικό αλυσίδας. Η εμπιστοσύνη εξακολουθεί να έρχεται αποκλειστικά από το system store και τα anchors που ρυθμίζετε
Το κενό που κλείνει αυτό εμφανίζεται την πρώτη φορά που επαληθεύετε PDF του πραγματικού κόσμου σε server Linux. Μεγάλο ποσοστό υπογραφόντων ενσωματώνει στο CMS μόνο το δικό του leaf πιστοποιητικό, οπότε το OpenSSL δεν φτάνει σε root, το TrustStatus επέρχεται άκυρο, και η ανάκληση δεν τρέχει ποτέ επειδή η αλυσίδα δεν έγινε ποτέ αξιόπιστη. Πριν το v3.121.0 το OpenSSL backend που περιγράφει το επαλήθευση υπογραφών PDF με OpenSSL στο PDFium VCL ήταν αυστηρά offline και το OnlineRetrieval δεν είχε καμία επίδραση σε αυτό. Ένα πράγμα αξίζει να ειπωθεί ευθύς: η ίδια η μηχανή PDFium δεν κάνει καθόλου επαλήθευση CMS, οπότε κάθε κανόνας παρακάτω ζει στη στρώση PAdES του component και στο binding OpenSSL του, εκεί που μπορείτε να τον διαβάσετε
Με τι σειρά επαληθεύει, ανακτά και ελέγχει το OpenSSL backend;
Πρώτα ακεραιότητα, μετά εμπιστοσύνη, μετά ανάκληση, και το δίκτυο αγγίζεται μόνο ανάμεσα στα βήματα που το χρειάζονται. Το VerifyCmsWithSsl ελέγχει την υπογραφή CMS και τα signed attributes (RFC 5652) με κατασταλειμένη την αξιολόγηση αλυσίδας, και αν εκείνο αποτύχει επιστρέφει αμέσως, πριν υπάρχει έστω session ανάκτησης, οπότε ένα έγγραφο με χαλασμένα bytes δεν προκαλεί καμία εξερχόμενη αίτηση. Μόνο αν η αλυσίδα μετά αποτύχει και το OnlineRetrieval είναι ανοιχτό ακολουθεί τους συνδέσμους AIA και επαληθεύει ξανά. Τα CRL distribution points ανακτώνται μόνο όταν η αλυσίδα είναι αξιόπιστη, επειδή ένα CRL κρεμασμένο σε μη αξιόπιστο μονοπάτι δεν αποδεικνύει τίποτα. Οι τρεις ετυμηγορίες μένουν ξεχωριστές σε όλη τη διαδρομή: μια έγκυρη υπογραφή με ημιτελή αλυσίδα αναφέρεται ακόμα ως έγκυρη υπογραφή
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER· η μόνη επιπλέον εμπιστοσύνη
ConfigureSslCmsVerifier;
Probe := TPdfCmsVerifyOptions.Default;
Probe.OnlineRetrieval := True;
Probe.CheckRevocation := True;
Diags := SslVerifyOptionsDiagnostics(Probe);
if psvdOnlineRetrievalIgnored in Diags then
Log('no HTTP transport or CMS_add1_cert: validation stays offline');
Trust := TPadesTrustValidationOptions.Default; // ptnpOffline ως προεπιλογή
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // ανά κλήση επαλήθευσης
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'signed-contract.pdf';
Pdf.Active := True;
Verdict := Pdf.ValidatePadesTrust(Trust);
for I := 0 to High(Verdict.Signatures) do
Log(Format('#%d trust=%d revocation=%d', [I,
Ord(Verdict.Signatures[I].CertificateTrustStatus),
Ord(Verdict.Signatures[I].RevocationStatus)]));
finally
Pdf.Free;
end;
end;
Γιατί τα κατεβασμένα πιστοποιητικά δεν μπαίνουν ποτέ στο trust store;
Επειδή τα URLs έρχονται από το πιστοποιητικό που επαληθεύεται, και ο υπογράφων τα διάλεξε. Η εγγραφή authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) είναι μια υπενθύμιση του πού ζει ο issuer, τίποτα περισσότερο. Αν ό,τι απαντά σε εκείνο το URL μπαινόταν στο store trust-anchors, ο καθένας θα μπορούσε να υπογράψει με αυτοσχέδιο κλειδί, να στρέψει το AIA στον δικό του server, και να πάρει πράσινη ετυμηγορία. Το RetrieveIntermediates περνά επομένως κάθε parsed πιστοποιητικό στο CMS_add1_cert, που το τοποθετεί στο μη αξιόπιστο σύνολο της συγκεκριμένης δομής CMS, και το OpenSSL εξακολουθεί να πρέπει να χτίσει μονοπάτι από εκείνο σε anchor που ρυθμίσατε ή που κρατά ήδη το system store. Υπάρχει και ένας πιο σιωπηλός λόγος: το όρισμα πιστοποιητικού του CMS_verify δεν είναι drop-in αντικατάστατο των πιστοποιητικών ενσωματωμένων στο CMS, οπότε η προσθήκη στο ίδιο το CMS είναι ο αξιόπιστος δρόμος
Ο βρόχος ανάκτησης είναι σκόπιμα στενός. Το RetrieveIntermediates τρέχει το πολύ 4 γύρους, καθένας από τους οποίους μάζεύει URLs caIssuers από κάθε πιστοποιητικό που βρίσκεται τώρα στο CMS, και σταματά μόλις ένας γύρος δεν προσθέσει τίποτα ή εξαντληθεί το budget χρόνου. Μια απάντηση πρέπει να κάνει decode με d2i_X509 ως ένα μοναδικό πιστοποιητικό DER που καταναλώνει ολόκληρο το σώμα· bytes στην ουρά απορρίπτονται, και ένα bundle PKCS#7 μόνο-πιστοποιητικά που σερβίρεται από URL .p7c προσπερνάται αντί να αποσυγκροτηθεί. Η μέθοδος πρόσβασης OCSP της ίδιας επέκτασης AIA αγνοείται, αφού αυτό το backend δεν μιλάει OCSP. Στην πλευρά της ανάκλησης, το RetrieveCrls διαβάζει μόνο τα URIs fullName κάθε DistributionPoint (RFC 5280 §4.2.1.13) από τα πιστοποιητικά του CMS και τα ρυθμισμένα anchors, και τα κατεβασμένα CRLs πηγαίνουν σε δεύτερο, ανεξάρτητο X509_STORE με έλεγχο CRL πλήρους αλυσίδας, οπότε ένα λείπαν ή παρωχημένο CRL αλλάζει το RevocationStatus χωρίς ποτέ να αγγίξει το TrustStatus
// Συμπυκνωμένο από το VerifyCmsWithSsl (FPdfCryptoSsl.pas)· η ρύθμιση BIO παραλείπεται.
// Κάθε κλήση _CMS_verify παίρνει φρέσκο content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // χαλασμένη υπογραφή: καθόλου δίκτυο
if Options.OnlineRetrieval and SslCapabilities.OnlineRetrieval then
FetchSession := TPdfCryptoFetchSession.Create(Options.UrlRetrievalTimeoutMs);
Store := BuildStore(False, nil, RevocationChecked, CrlsMalformed);
if (_CMS_verify(Cms, nil, Store, Bio, nil, CMS_BINARY) <> 1) and
(FetchSession <> nil) then
begin
RetrieveIntermediates(Cms, FetchSession); // CMS_add1_cert, μόνο untrusted
// επαληθεύστε την αλυσίδα ξανά απέναντι στο ίδιο anchor store
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// δεύτερο, ξεχωριστό store: ρυθμισμένα CRLs συν τα ανακτημένα
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Πόσο κοστίζει το πολύ μία κλήση επαλήθευσης;
Ένα σταθερό ταβάνι, επιβαλλόμενο από ένα TPdfCryptoFetchSession που τα βήματα AIA και CRL μιας ενιαίας κλήσης επαλήθευσης μοιράζονται. Τα όρια είναι σταθερές στο FPdfCryptoHttp, όχι προτάσεις:
- Χρόνος: το
UrlRetrievalTimeoutMs, που έχει προεπιλογή 15000 τόσο στοTPdfCmsVerifyOptions.Defaultόσο και στοTPadesTrustValidationOptions.Default· ένα session που δημιουργείται με 0 πέφτει πίσω στο 30000, και το ρολόι ξεκινά μόλις η υπογραφή περάσει, καλύπτοντας κάθε επόμενη αίτηση - Requests: το πολύ 8 ανά session, μετρημένα πριν επιχειρηθεί η μεταφορά, οπότε ένας νεκρός host εξακολουθεί να καίει μια θέση
- Bytes: 1 MiB ανά απάντηση και 4 MiB σύνολο, με URLs πάνω από 2048 χαρακτήρες να απορρίπτονται πριν από οποιαδήποτε σύνδεση
Η λογιστική είναι αυστηρότερη από ό,τι φαίνεται με την πρώτη ματιά. Τα bytes που λαμβάνονται από αποτυχημένη απάντηση μετράνε στο σύνολο, οπότε ένας server που απαντά 404 με μεγάλη σελίδα δεν στραγγίζει το budget δωρεάν. Η ανάγνωση που ξεπερνά το όριο ανά απάντηση εγκαταλείπει το κατέβασμα αντί να περάσει κομμένο σώμα στον parser ASN.1, και ένα HTTP 200 με κενό σώμα απορρίπτεται ολότελα, επειδή το μονοπάτι AIA διαφορετικά θα έκανε index στο Data[0] κενού πίνακα. Μόνο σκέτα URLs http:// και https:// περνούν, χωρίς redirects, cookies, credentials ή αυτόματη ανακάλυψη proxy, ενώ το HTTPS κρατά τους συνήθεις ελέγχους πιστοποιητικού και hostname. Η αφαίρεση διπλότυπων URLs είναι σκόπιμα περιορισμένη σε μία κλήση: η επόμενη επαλήθευση πρέπει να μπορεί να δει ένα CRL μόλις δημοσιευμένο. Το budget είναι επίσης ανά κλήση, όχι ανά έγγραφο, και το ValidatePadesTrust επαληθεύει κάθε υπογραφή και κάθε token timestamp χωριστά, οπότε η χειρότερη περίπτωση μεγαλώνει με τον αριθμό των υπογραφών
Γιατί ένα request WinHTTP που έληξε μπορεί ακόμα να γράψει στη μνήμη σας;
Επειδή η επιστροφή σε timeout δεν ακυρώνει τα callbacks που είναι ήδη εν πτήσει. Η μεταφορά Windows οδηγεί το WinHTTP ασύγχρονα και περιμένει σε event με τον υπόλοιπο χρόνο του session, και όταν εκείνη η αναμονή τα παρατήσει, το request μπορεί ακόμα να ολοκληρώσει μια ανάγνωση και να σηματοδοτήσει μετά. Στρέψτε την ασύγχρονη ανάγνωση σε buffer stack και εκείνο το καθυστερημένο completion γράφει σε frame που ανήκει τότε σε κάποια άσχετη συνάρτηση. Το fix είναι ownership, όχι timing: το event και ο buffer ανάγνωσης 16 KB ζουν σε heap εγγραφή με δύο αναφορές, μία που κρατά ο καλών και μία που ελευθερώνεται μόνο από το τελικό callback HANDLE_CLOSING, οπότε όποια πλευρά τελειώσει τελευταία ελευθερώνει τη μνήμη
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // caller + τελικό callback HANDLE_CLOSING
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // εδώ προσγειώνονται τα async reads, ποτέ σε stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// Στο callback κατάστασης: το HANDLE_CLOSING είναι η τελευταία ειδοποίηση που
// στέλνει το WinHTTP για το request, οπότε ρίχνει τη δεύτερη αναφορά
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Τι πρέπει να παρέχει το libcurl σε FPC Unix
Ασύγχρονο resolver και build με ασφάλεια νημάτων, ή η online ανάκτηση μένει κλειστή. Σε FPC Unix η μεταφορά περνά από libcurl, την ίδια εξάρτηση πίσω από το libcurl timestamp backend για στόχους μη Windows, και το binding αρνείται οποιαδήποτε βιβλιοθήκη της οποίας η μάσκα χαρακτηριστικών στερείται CURL_VERSION_ASYNCHDNS ή CURL_VERSION_THREADSAFE. Ο λόγος είναι ότι το CURLOPT_NOSIGNAL, που μια βιβλιοθήκη μέσα στη διαδικασία κάποιου άλλου πρέπει να θέτει, σε συνδυασμό με σύγχρονο resolver σημαίνει ότι μια αναζήτηση DNS μπορεί απλώς να επιβιώσει του timeout. Η δεύτερη παγίδα είναι το shutdown: το curl_global_cleanup δεν περιμένει τα ασύγχρονα νήματα DNS, οπότε από τη στιγμή που το libcurl έχει αρχικοποιηθεί η module μένει φορτωμένη μέχρι η διαδικασία να τερματίσει αντί να αφήσει ένα background νήμα να τρέξει σε unloaded κώδικα. Όταν αποτύχει οποιαδήποτε από τις δύο απαιτήσεις, το SslCapabilities.OnlineRetrieval είναι False και το SslVerifyOptionsDiagnostics αναφέρει psvdOnlineRetrievalIgnored αντί να προσποιηθεί ότι συμβουλεύτηκε το δίκτυο
Τι εγγυάται και τι δεν εγγυάται το αποτέλεσμα
Ένα έγκυρο RevocationStatus από αυτό το backend σημαίνει ότι βρέθηκαν, ρυθμίστηκαν ή κατέβηκαν τρέχοντα CRLs που καλύπτουν ολόκληρη την αλυσίδα, και κανένα δεν απαριθμούσε πιστοποιητικό της· τίποτα περισσότερο. Δεν υπάρχει OCSP, οπότε ένα CA που δημοσιεύει ανάκληση μόνο μέσω OCSP αφήνει το αποτέλεσμα μη υποστηριζόμενο, και μια αποτυχία δικτύου μοιάζει ακριβώς με CA που δεν δημοσιεύει τίποτα. Σημειώστε επίσης ότι το psvdNoCrlsConfigured περιγράφει μόνο τα CRLs που ρυθμίσατε εσείς, οπότε με online ανάκτηση είναι υπόδειξη, όχι πρόγνωση αποτυχίας. Όταν ένα audit trail πρέπει να είναι αναπαραγώγιμο χωρίς πρόσβαση δικτύου, αφήστε το NetworkPolicy στο default του ptnpOffline: κανένα session ανάκτησης δεν δημιουργείται και το backend δεν ανοίγει ποτέ σύνδεση, που ταιριάζει το offline συμβόλαιο στην πλευρά CryptoAPI που περιγράφει ο έλεγχος ανάκλησης υπογραφών PDF offline στα Windows
Ο κώδικας ανάκτησης, τα budgets και τα bindings των μεταφορών έρχονται ως πηγαίος κώδικας με το PDFium Delphi component, οπότε μπορείτε να επιβεβαιώσετε ακριβώς ποια URLs μπορεί να επικοινωνήσει μια επαλήθευση και πόσο μπορεί να κατεβάσει πριν ενεργοποιήσετε το ptnpOnline σε server που χειρίζεται μη αξιόπιστα έγγραφα