Ένας επικυρωτής (validator) απορρίπτει μια υπογραφή PAdES για έναν από τρεις λόγους σε σχεδόν κάθε περίπτωση που έχουμε αποσφαλματώσει με το PDFium Component στο Delphi: ο πίνακας /ByteRange εξακολουθεί να περιέχει τους μηδενικούς του δείκτες θέσης, ο χρόνος υπογραφής /M δεν είναι μια καλά σχηματισμένη συμβολοσειρά ημερομηνίας PDF, ή μια σταδιακή ενημέρωση αφαίρεσε την καταχώρηση /Encrypt από ένα κρυπτογραφημένο έγγραφο. Και τα τρία παράγουν αρχεία που ανοίγουν μια χαρά, αποδίδονται μια χαρά και αποτυγχάνουν τη στιγμή που ένας συμβατός επικυρωτής διαβάζει το λεξικό της υπογραφής
Το σενάριο που αποτελεί την αφορμή για αυτό το άρθρο είναι εξαιρετικά συγκεκριμένο. Υπογράφετε ένα συμβόλαιο με τη ροή εργασίας από το άρθρο για την υπογραφή PAdES B-B, ο δικός σας κώδικας επιθεώρησης αναφέρει ότι η υπογραφή είναι παρούσα και δομικά υγιής, κάθε τοπική δοκιμή είναι πράσινη — και στη συνέχεια ο αντισυμβαλλόμενος ανεβάζει το αρχείο στην πλατφόρμα επικύρωσής του και λαμβάνει έναν κόκκινο σταυρό. Τίποτα σχετικά με την αποτυχία δεν είναι ορατό σε ένα πρόγραμμα προβολής, επειδή κανένα από αυτά τα τρία ελαττώματα δεν αγγίζει το περιεχόμενο της σελίδας. Ζουν εξ ολοκλήρου στο λεξικό υπογραφής και στο trailer του αρχείου, που είναι ακριβώς εκεί όπου κοιτάζουν οι επικυρωτές και τα προγράμματα προβολής ως επί το πλείστον όχι
Γιατί η υπογραφή PDF ByteRange μου είναι άκυρη;
Μια απόρριψη του ByteRange σημαίνει σχεδόν πάντα ότι τα τέσσερα πεδία δεν συμπληρώθηκαν ποτέ, και όχι ότι τα εύρη είναι ελαφρώς λάθος. Ο πίνακας /ByteRange [A B C D] δηλώνει δύο διαστήματα, τα byte A έως A+B και τα byte C έως C+D, και το EN 319 142-1 §6.3 (απαίτηση k) απαιτεί μαζί να καλύπτουν ολόκληρο το αρχείο εκτός από τη δεκαεξαδική κωδικοποιημένη συμβολοσειρά /Contents. Σε αριθμούς: το A είναι 0, το C >= A+B, το C+D ισούται με το μήκος του αρχείου, και το κενό μεταξύ B και C περιέχει ακριβώς τη δεκαεξαδική συμβολοσειρά <...> — δύο byte αγκυλών συν δύο δεκαεξαδικούς χαρακτήρες ανά byte δεδομένων CMS. Ένας επικυρωτής που διαβάζει [0 0 0 0] συμπεραίνει ότι η υπογραφή δεν καλύπτει τίποτα και την απορρίπτει, ανεξάρτητα από το πόσο σωστή είναι η δομή CMS μέσα στο /Contents
Η μηχανική του πώς συμβαίνει αυτό αξίζει να γίνει κατανοητή επειδή το ίδιο μοτίβο υπάρχει σε κάθε στοίβα υπογραφής. Ένας υπογράφων δεν μπορεί να γνωρίζει τις τελικές μετατοπίσεις (offsets) έως ότου διαμορφωθεί το αρχείο, οπότε ο συγγραφέας εκπέμπει τον πίνακα με μηδενικούς δείκτες θέσης σταθερού πλάτους και τους συμπληρώνει εκ των υστέρων μετά τη διαμόρφωση. Στις εκδόσεις του PDFium Component πριν από την 2.14.1, αυτή η συμπλήρωση εκ των υστέρων αναζητούσε το μοτίβο δείκτη θέσης ξεκινώντας από τη θέση /Contents — αλλά το /ByteRange βρίσκεται πριν από το /Contents στο λεξικό, οπότε η αναζήτηση δεν βρήκε τίποτα και οι τρεις αντικαταστάσεις απέτυχαν σιωπηλά. Η σύνοψη (digest) CMS υπολογίστηκε πάνω στα σωστά εύρη, οπότε η κρυπτογραφία ήταν υγιής· η δήλωση αυτών των ευρών παρέμεινε στο μηδέν, οπότε κάθε συμβατός επικυρωτής απέρριπτε το αρχείο. Οι χρονικές σημάνσεις εγγράφων PAdES B-LTA, που χρησιμοποιούν την ίδια διάταξη λεξικού, απέτυχαν με τον ίδιο τρόπο — σχετικό εάν βασίζεστε σε μακροπρόθεσμες υπογραφές B-LT και B-LTA. Η έκδοση 2.14.1 συμπληρώνει εκ των υστέρων από την έναρξη του αντικειμένου υπογραφής, και η διόρθωση καλύπτεται από μια δοκιμή παλινδρόμησης που αναλύει το παραγόμενο αρχείο και επιβεβαιώνει την παρακάτω αριθμητική
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Είκοσι γραμμές απλού κώδικα RTL, χωρίς κλήσεις βιβλιοθήκης, και συλλαμβάνει ολόκληρη την κατηγορία αποτυχίας στο σημείο παραγωγής. Εάν κρατήσετε μία επιβεβαίωση (assertion) από αυτό το άρθρο, κρατήστε αυτήν: αναλύστε το δικό σας υπογεγραμμένο αποτέλεσμα και ελέγξτε τις τρεις ισότητες πριν το αρχείο φύγει από τη διαδικασία σας
Γιατί οι επικυρωτές επισημαίνουν τον χρόνο υπογραφής /M ως κακοσχηματισμένο;
Οι αυστηροί επικυρωτές απορρίπτουν έναν χρόνο υπογραφής που δεν είναι μια πλήρης συμβολοσειρά ημερομηνίας PDF, και τα δύο μέρη που λείπουν συχνότερα είναι το πρόθεμα D: και ο δείκτης μετατόπισης UTC. Το ISO 32000-1 §7.9.4 ορίζει τη μορφή ημερομηνίας ως D:YYYYMMDDHHmmSS ακολουθούμενη από μια μετατόπιση — Z για UTC, ή μια υπογεγραμμένη σχέση +HH'mm' με αυτό. Ένα σκέτο 20260709143000 αναλύεται ως ημερομηνία από έναν επιεική αναγνώστη, αλλά ένας επικυρωτής που εφαρμόζει τη γραμματική κυριολεκτικά βλέπει μια κακοσχηματισμένη συμβολοσειρά σε ένα πεδίο υποχρεωτικής μορφής και επισημαίνει την υπογραφή. Οι εκδόσεις του PDFium Component πριν από την 2.14.4 έγραφαν την καταχώρηση /M των TPdf.SignPades και SignPadesBytes ακριβώς σε αυτήν τη σκέτη μορφή· από την 2.14.4 η καταχώρηση φέρει το πρόθεμα D: και τον δείκτη Z, οπότε ο δηλωμένος χρόνος υπογραφής διαβάζεται ως D:20260709143000Z
Δύο πρακτικές σημειώσεις συνοδεύουν αυτό το πεδίο. Πρώτον, γράψτε UTC και δηλώστε το: μια χρονική σήμανση χωρίς δείκτη μετατόπισης αναγκάζει τον επικυρωτή να μαντέψει τη σχέση της ζώνης ώρας, και η §7.9.4 αντιμετωπίζει τη μετατόπιση ως μέρος της μορφής και όχι ως προαιρετική λεπτομέρεια. Δεύτερον, θυμηθείτε τι είναι το /M — ο δηλωμένος από τον υπογράφοντα χρόνος, ένας ισχυρισμός και όχι μια απόδειξη. Ένας επικυρωτής ελέγχει τη μορφή του εδώ, όχι την αλήθεια του· ο αποδείξιμος χρόνος προέρχεται από μια χρονική σήμανση κατά RFC 3161 σε PAdES B-T και άνω. Η σωστή μορφοποίηση ενός πεδίου και η εμπιστοσύνη σε αυτό είναι ξεχωριστές αποφάσεις, και οι επικυρωτές επιβάλλουν μόνο την πρώτη
Γιατί το SignPades προκαλεί EPadesCrypto σε ένα κρυπτογραφημένο PDF;
Το PDFium Component αρνείται να υπογράψει ένα κρυπτογραφημένο έγγραφο επειδή η εναλλακτική λύση είναι η παραγωγή ενός αρχείου που οι συμβατοί αναγνώστες θα καταστρέψουν κατά το άνοιγμα. Μια υπογραφή PAdES προσαρτάται ως σταδιακή ενημέρωση, και το ISO 32000-1 §7.5.6 απαιτεί το νέο trailer ενός τμήματος ενημέρωσης να φέρει κάθε καταχώρηση του προηγούμενου trailer εκτός από το /Prev — σε ένα κρυπτογραφημένο έγγραφο αυτό περιλαμβάνει το /Encrypt. Εάν το παραλείψετε, το πιο πρόσφατο trailer δηλώνει το αρχείο ως μη κρυπτογραφημένο, οπότε ένας συμβατός αναγνώστης αναλύει το κρυπτογραφημένο σώμα ως απλό κείμενο και λαμβάνει σκουπίδια. Ακόμα χειρότερα, οι ροές και οι συμβολοσειρές που προσαρτά ο υπογράφων θα έπρεπε οι ίδιες να είναι κρυπτογραφημένες με το κλειδί του εγγράφου για να είναι νόμιμες, κάτι που ένας εγχυτής υπογραφής απλού κειμένου δεν μπορεί να κάνει. Δεν υπάρχει τρόπος να προσαρτηθεί μια έγκυρη υπογραφή απλού κειμένου σε ένα κρυπτογραφημένο αρχείο, οπότε από την έκδοση 2.14.2 οι μέθοδοι TPdf.SignPades, SignPadesBytes και InjectPadesDssMarkers προκαλούν EPadesCrypto αντί να εκπέμπουν ένα κατεστραμμένο ή μη επαληθεύσιμο αποτέλεσμα
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
Η εξαίρεση είναι το σωστό αποτέλεσμα, επομένως σχεδιάστε τη ροή εργασίας γύρω από αυτήν αντί να την συλλαμβάνετε και να προσπαθείτε ξανά. Αποκρυπτογραφήστε πρώτα με εξουσιοδότηση ιδιοκτήτη, υπογράψτε το αντίγραφο απλού κειμένου και εάν το κανάλι διανομής απαιτεί κρυπτογράφηση, αποδεχτείτε ότι η κρυπτογράφηση-μετά-υπογραφή και η υπογραφή-μετά-κρυπτογράφηση παράγουν διαφορετικά τεχνουργήματα με διαφορετικές ιστορίες επικύρωσης. Μια υπογραφή που υπολογίζεται πάνω σε byte απλού κειμένου δεν μπορεί να επιβιώσει από την εκ νέου κρυπτογράφηση αυτών των byte, οπότε οι ειλικρινείς επιλογές είναι ένα υπογεγραμμένο αρχείο απλού κειμένου ή μια απόφαση πολιτικής τεκμηριωμένη δίπλα στον κώδικα
Πώς δύο σφάλματα μπορούν να πιστοποιήσουν το ένα το άλλο
Το ελάττωμα του ByteRange παρέμεινε κρυφό για τόσο καιρό επειδή ο δικός μας επικυρωτής ήταν λάθος με συμπληρωματικό τρόπο, και αυτό είναι το πιο μεταβιβάσιμο μάθημα σε αυτό το άρθρο. Ο έλεγχος κάλυψης στο ValidatePadesCompliance απαιτούσε το δεύτερο διάστημα να ξεκινά ακριβώς στο A+B — ένα μηδενικό κενό — το οποίο ταξινομεί εσφαλμένα την τυπική διάταξη όπου το κενό περιέχει τη δεκαεξαδική συμβολοσειρά /Contents. Έτσι, ο εσωτερικός επικυρωτής απέρριπτε ακριβώς τη συμβατή διάταξη και η εσωτερική γεννήτρια δεν παρήγαγε ποτέ καμία, και οι αλυσίδες υπογραφής-και-επικύρωσης έτυχε να παραμένουν πράσινες. Κάθε σφάλμα στερούσε από τη σουίτα δοκιμών το αντίθετο παράδειγμα που θα αποκάλυπτε το άλλο. Η έκδοση 2.14.1 διόρθωσε και τις δύο πλευρές στην ίδια κυκλοφορία: η γεννήτρια συμπληρώνει εκ των υστέρων και τα τέσσερα πεδία, και ο επικυρωτής δέχεται C >= A+B με το C+D ίσο με το μήκος του αρχείου
Η μεθοδολογική διορθώση είναι η διασταυρούμενη επικύρωση (cross-validation) έναντι μιας υλοποίησης που δεν γράψατε εσείς. Μια γεννήτρια και ένας επικυρωτής που μοιράζονται μια βάση κώδικα, έναν συγγραφέα ή ακόμα και ένα νοητικό μοντέλο της μορφής μπορούν να συμφωνούν σε μια κοινή παρεξήγηση επ' αόριστον· ένας ανεξάρτητος επικυρωτής σπάει τη συμμετρία. Περάστε το υπογεγραμμένο αποτέλεσμά σας από τουλάχιστον έναν εξωτερικό ελεγκτή συμμόρφωσης πριν από μια κυκλοφορία, και κρατήστε έναν δομικό αυτοέλεγχο στη δημιουργία ως τη γρήγορη πρώτη γραμμή — η TPdf.ValidatePades, που περιγράφεται στο άρθρο επιθεώρησης υπογραφής, αναφέρει την αποτυχία κάλυψης ως συγκεκριμένο ζήτημα
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
Μια λίστα ελέγχου απόρριψης για υπογεγραμμένο αποτέλεσμα
Όταν μια πλατφόρμα απορρίπτει την υπογραφή PAdES σας, ελέγξτε τις απλές δομικές αιτίες πριν υποψιαστείτε πιστοποιητικά ή αλυσίδες εμπιστοσύνης. Διαβάστε τον πίνακα /ByteRange και επαληθεύστε τις τρεις ισότητες: πρώτο πεδίο μηδέν, δεύτερο διάστημα που ξεκινά στο τέλος του πρώτου ή μετά από αυτό, δεύτερο διάστημα που τελειώνει ακριβώς στο τέλος του αρχείου. Διαβάστε την καταχώρηση /M και επαληθεύστε ότι είναι μια πλήρης συμβολοσειρά ημερομηνίας της παραγράφου §7.9.4 με το πρόθεμα D: και έναν δείκτη μετατόπισης. Επιβεβαιώστε ότι το έγγραφο προέλευσης δεν ήταν κρυπτογραφημένο όταν προσαρτήθηκε η υπογραφή — και εάν η στοίβα σας το υπέγραψε ούτως ή άλλως χωρίς παράπονο, αντιμετωπίστε αυτή τη σιωπή ως σφάλμα στη στοίβα. Και οι τρεις έλεγχοι εκτελούνται σε ακατέργαστα byte σε χιλιοστά του δευτερολέπτου, και κατά την εμπειρία μας εξηγούν την απόρριψη πολύ πιο συχνά από οποιαδήποτε κρυπτογραφική αιτία
Οι κλήσεις υπογραφής, επιθεώρησης και επικύρωσης που χρησιμοποιούνται εδώ — SignPades, ValidatePades και οι έλεγχοι συμμόρφωσης PAdES με τη διορθωμένη αριθμητική ByteRange, τη μορφοποίηση ημερομηνίας και την πύλη κρυπτογράφησης — παρέχονται με το PDFium Component για Delphi, C++Builder και Lazarus