Ένα workbench που συνδέει την επικύρωση συμμόρφωσης με την ψηφιακή υπογραφή πρέπει να συντονίσει τέσσερα βήματα, με αυτή τη σειρά, και να τα κρατήσει δεμένα σε ένα και μόνο σύνολο bytes σε όλη τη διαδρομή. Εκτελεί έναν έλεγχο preflight PDF/A ή PDF/UA. Εφαρμόζει όποιες διορθώσεις απαιτούν τα ευρήματα και αποθηκεύει μια διορθωμένη αναθεώρηση. Υπογράφει ακριβώς αυτή την αναθεώρηση. Έπειτα διαβάζει ξανά το υπογεγραμμένο αρχείο και επιβεβαιώνει ότι η υπογραφή το καλύπτει πραγματικά. Η σειρά δεν είναι διακοσμητική. Παραλείψτε την επανανάγνωση και εμπιστεύεστε απλώς τη δική σας διαδρομή εγγραφής· αφήστε τον preflight να τρέξει πάνω σε λάθος αναθεώρηση και η αναφορά συμμόρφωσης θα περιγράφει ένα αρχείο που δεν παραδώσατε ποτέ
Το σημείο που οι περισσότερες αυτοσχέδιες ροές το χάνουν είναι η ραφή ανάμεσα στην επικύρωση και την υπογραφή. Τρέξτε τα ως δύο ξεχωριστά εργαλεία με ένα πέρασμα αποκατάστασης ενδιάμεσα και θα δημιουργηθούν τουλάχιστον τρεις διακριτές αναθεωρήσεις του αρχείου, καθεμία με τα δικά της bytes. Η αναφορά preflight που δίνετε σε έναν ελεγκτή περιγράφει τη μία. Η υπογραφή παγώνει μια άλλη. Τίποτα μέσα στο αρχείο δεν δηλώνει ότι πρόκειται για την ίδια αναθεώρηση, και συχνά δεν είναι. Το PDF Library for Delphi, η losLab PDF Developer Library για Delphi και C++Builder, τοποθετεί τον preflight και την υπογραφή PAdES πίσω από μία ενιαία facade class, ώστε ολόκληρη η ακολουθία να ζει σε μία διεργασία που δεν χάνει ποτέ το ίχνος του ποια bytes αφορά. Κάθε κλήση παρακάτω υπάρχει στη βιβλιοθήκη σήμερα, όπως και κάθε παγίδα που σημειώνεται δίπλα της
Τρεις αναθεωρήσεις ενός εγγράφου, και πώς ανοίγει το κενό
Μετρήστε τις αποθηκεύσεις. Το πρωτότυπο φτάνει από το upstream. Το πέρασμα αποκατάστασης το φορτώνει, ενεργοποιεί μια λειτουργία συμμόρφωσης και γράφει μια διορθωμένη αναθεώρηση. Το πέρασμα υπογραφής προσαρτά μια υπογραφή ως incremental update, που είναι μια τρίτη εγγραφή. Τρεις αποθηκεύσεις, τρεις διατάξεις bytes, και μια αναφορά preflight δεν σημαίνει τίποτα αν δεν κατονομάζει ποια από τις τρεις καλύπτει. Ένα SHA-256 του αρχείου, καταγεγραμμένο δίπλα σε κάθε εκτέλεση preflight και σε κάθε υπογραφή, είναι η φθηνή άγκυρα που σας επιτρέπει να αποδείξετε ότι η αναθεώρηση που επικυρώσατε είναι η αναθεώρηση που υπογράψατε
Μια συμπεριφορά της βιβλιοθήκης σφίγγει ακόμη περισσότερο αυτή την πειθαρχία. Οι διορθώσεις συμμόρφωσης που ζητούνται μέσω SetPDFAMode ή SetPDFUAMode δεν ενεργοποιούνται τη στιγμή που τις καλείτε. Εφαρμόζονται κατά την αποθήκευση. Αυτόματες επιδιορθώσεις όπως η επιβολή print flags στα annotations ή η ανάθεση σειράς tab για PDF/UA καταλήγουν στο αρχείο εξόδου και πουθενά αλλού, οπότε ένας έλεγχος πάνω στο έγγραφο που μόλις «διορθώσατε» στη μνήμη δεν σας λέει τίποτα για τα bytes που κατευθύνονται προς τον υπογράφοντα. Αποθηκεύστε πρώτα, μετά κάντε preflight στο αποθηκευμένο αρχείο. Η κατάσταση στη μνήμη είναι πρόχειρο· μόνο το αρχείο στον δίσκο είναι πραγματικό
Preflight από τον δίσκο, και το μηδέν που σημαίνει δύο πράγματα
Το flat σημείο εισόδου του preflight είναι το CheckFileCompliance(FileName, Password, ComplianceTest, Options). Ο έλεγχος 1 επιλέγει PDF/A (ISO 19005), ο έλεγχος 2 επιλέγει PDF/UA (ISO 14289). Ανοίγει το αρχείο μέσω του streaming reader της βιβλιοθήκης, οπότε δεν χρειάζεται πρώτα LoadFromFile, και επιστρέφει ένα handle λίστας συμβολοσειρών που φέρει ένα εύρημα ανά καταχώριση:
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
Η παγίδα βρίσκεται στην τιμή επιστροφής, και είναι από αυτές που περνούν κάθε happy-path έλεγχο. Το μηδέν σημαίνει «κανένα εύρημα». Το μηδέν σημαίνει επίσης «το αρχείο δεν μπόρεσε να ανοίξει», γιατί η υλοποίηση επιστρέφει 0 όποτε η λίστα αποτελεσμάτων γυρίζει άδεια, συμπεριλαμβανομένης μιας αποτυχίας ανάγνωσης. Ένα workbench που διαβάζει το 0 ως πράσινο φως θα εγκρίνει ανέμελα ένα αρχείο που κάποια άλλη διεργασία έχει κλειδώσει. Ο συνδυασμός της κλήσης με το LastErrorCode, όπως παραπάνω, είναι αυτό που ξεχωρίζει τις δύο περιπτώσεις. Ο ελεγκτής ανοίγει επίσης το αρχείο με share mode που απαγορεύει την εγγραφή, οπότε αν το βήμα αποκατάστασης κρατά ακόμη ένα writer handle, ο preflight αποτυγχάνει για λόγο που δεν έχει καμία σχέση με τη συμμόρφωση και κάθε σχέση με ένα stream που ξεχάσατε να απελευθερώσετε
Όταν τα ευρήματα πρέπει να τα διαβάσει άνθρωπος και όχι μια ροή, το CreatePreflightReport τα αποδίδει ως ευανάγνωστη αναφορά. Το ComparePreflightReports συγκρίνει δύο εκτελέσεις, που είναι ένας τακτικός τρόπος να δείξετε ότι η αποκατάσταση καθάρισε τα αρχικά ευρήματα χωρίς να εισαγάγει αθόρυβα καινούργια
Υπογραφή της ελεγμένης αναθεώρησης με ένα SignProcess
Μόλις η αποθηκευμένη αναθεώρηση περάσει τον preflight και το hash της καταγραφεί, υπογράψτε ακριβώς αυτό το αρχείο και κανένα άλλο. Το SignProcess API διαβάζεται σαν builder. Ανοίγετε ένα handle διεργασίας, το ρυθμίζετε γραμμή προς γραμμή, το οριστικοποιείτε και μετά διαβάζετε πίσω τον κωδικό αποτελέσματος
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // προφίλ PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // χώρος για μεταγενέστερη χρονοσφραγίδα
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Δύο γραμμές σε αυτή την ακολουθία έχουν μεγαλύτερο βάρος απ' όσο δείχνουν. Το SetSignProcessCustomSubFilter με ETSI.CAdES.detached επιλέγει υπογραφή PAdES όπως προδιαγράφεται στο ETSI EN 319 142-1, αντί για την παλαιά οικογένεια adbe.pkcs7.detached, και εκεί βρίσκεται η διαφορά ανάμεσα σε μια υπογραφή που ένας ευρωπαϊκός validator αποδέχεται και σε μία που την επισημαίνει ως προβληματική. Το SetSignProcessReserveContentsBytes γεμίζει το placeholder /Contents, και το μέγεθος που επιλέγετε εδώ είναι απόφαση για το μέλλον: αν πρόκειται ποτέ να ακολουθήσει χρονοσφραγίδα υπογραφής, το διευρυμένο CMS πρέπει να χωρέσει στον χώρο που δεσμεύετε τώρα, γιατί το placeholder δεν μπορεί να μεγαλώσει αργότερα χωρίς να ξαναϋπογραφεί ολόκληρο το έγγραφο. Δεσμεύστε γενναιόδωρα και σπαταλάτε λίγα kilobytes. Δεσμεύστε πολύ στενά και το βήμα της χρονοσφραγίδας θα αποτύχει μήνες αργότερα με μια υπερχείλιση που δύσκολα θα συνδέσετε πίσω με αυτή τη μία γραμμή
Το GetSignProcessResult απαντά με κωδικό, όχι με boolean, και οι κωδικοί αξίζει να κρατηθούν. Το 1 σημαίνει επιτυχία. Το 4 είναι λάθος κωδικός PDF, το 7 λάθος κωδικός πιστοποιητικού, το 9 ένα PFX που δεν φέρει ιδιωτικό κλειδί, το 11 αποτυχία κατά την εφαρμογή της υπογραφής. Συμπτύξτε τα σε ένα true/false και πετάτε την πληροφορία που ξεχωρίζει μια περίπτωση υποστήριξης με λάθος κωδικό από μία με κλειδί χωρίς ιδιωτικό μέρος. Καταγράψτε τον ακέραιο
Επανανάγνωση: έλεγχος του αρχείου που μόλις παρήγατε
Κανένα workbench δεν πρέπει να εμπιστεύεται τη διαδρομή που έγραψε το αρχείο το οποίο πρόκειται να πιστοποιήσει. Η κλάση ελέγχου TPDFlibSignDoc ξανανοίγει την υπογεγραμμένη έξοδο και διαβάζει τις καταχωρίσεις του signature dictionary απευθείας από τον δίσκο:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Καταγράψτε το μέγεθος πριν από το Open: το αντικείμενο ελέγχου κρατά share lock στο αρχείο
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 σημαίνει ότι το πεδίο είναι υπογεγραμμένο
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Τα ορίσματα ValueKey αντιστοιχούν σε καταχωρίσεις του dictionary. Το κλειδί 0 επιστρέφει το ακατέργαστο CMS από το /Contents, τα κλειδιά 2 και 3 τα ονόματα /Filter και /SubFilter, και τα 11 έως 14 τους τέσσερις αριθμούς του ByteRange. Οι τιμές κειμένου επιστρέφονται αντ' αυτού μέσω GetSignatureTextValueByName: το κλειδί 0 είναι η δηλωμένη ώρα υπογραφής, και το κλειδί 5 ξεχωρίζει μια συνηθισμένη Sig από ένα DocTimeStamp, κάτι που μετράει μόλις ένα έγγραφο φέρει και τα δύο
Η καταγραφή του μεγέθους αρχείου στην αρχή εκείνου του παραδείγματος είναι φέρουσα, όχι απλή τακτοποίηση. Το TPDFlibSignDoc.Open κρατά το αρχείο κάτω από περιοριστικό share lock για όλη τη διάρκεια ζωής του, οπότε οτιδήποτε χρειάζεται τα ακατέργαστα bytes (υπολογισμός hash του υπογεγραμμένου εύρους, εκ νέου υπολογισμός του CMS digest) πρέπει να διαβάσει το αρχείο πριν κληθεί το Open. Το ίδιο το demo SigningWorkbench της βιβλιοθήκης διαβάζει πρώτα ολόκληρο το αρχείο στη μνήμη ακριβώς γι' αυτόν τον λόγο, και ένα workbench που αγνοεί τη σειρά αποτυγχάνει κατά διαστήματα, σε όποιο μηχάνημα τύχει να χάσει τον αγώνα ταχύτητας
Αριθμητική του ByteRange που αποδεικνύει την κάλυψη
Ένα υγιές αρχείο με μία υπογραφή έχει ByteRange της μορφής [0 a b c]: η κάλυψη ξεκινά στο offset 0, παρακάμπτει το δεκαεξαδικό placeholder /Contents ανάμεσα στο a και το b, και ύστερα συνεχίζει έως το byte b+c. Όταν το b+c ισούται με το μέγεθος του αρχείου, η υπογραφή καλύπτει τα πάντα μέχρι το τέλος του αρχείου, που είναι το αποτέλεσμα που θέλετε. Όταν υπολείπεται, κάποιος προσάρτησε ένα incremental update μετά την εγγραφή της υπογραφής. Αυτό είναι απολύτως θεμιτό κατά το ISO 32000-1§12.8, αφού μεταγενέστερες συμπληρώσεις φορμών, μια δεύτερη υπογραφή και ένα DSS dictionary φτάνουν ακριβώς με αυτόν τον τρόπο. Είναι επίσης ακριβώς το γεγονός που ένα audit trail οφείλει να καταγράφει τη στιγμή της υπογραφής, αντί να το ανακατασκευάζει υπό πίεση στη διάρκεια μιας διαφωνίας
Προσέξτε το πλάτος των ακεραίων καθώς κάνετε αυτή την αριθμητική. Το GetSignProcessByteRange του flat API επιστρέφει Integer 32 bit, αλλά οι υποκείμενες τιμές είναι Int64, οπότε σε αρχείο πάνω από 2 GB ο flat accessor αποκόπτει σιωπηλά. Καταφύγετε στο TPDFlibSigner.GetByteRange του επιπέδου κλάσεων, που επιστρέφει Int64, ή αναλύστε τις τιμές από το GetSignatureValueByName όπως κάνει ο κώδικας ελέγχου παραπάνω
Τι αφήνει η βιβλιοθήκη σε εσάς
Δύο όρια είναι προτιμότερο να τα μάθετε στη φάση του σχεδιασμού παρά στο τελευταίο sprint. Το flat API TPDFlib δεν φέρει κανένα wrapper επαλήθευσης υπογραφών. Η κρυπτογραφική επαλήθευση βρίσκεται ένα επίπεδο πιο κάτω, στο TPDFlibSignatureVerifier, του οποίου το VerifySignature απαντά έγκυρη, άκυρη ή άγνωστη. Δεν υπάρχει επίσης ενσωματωμένος HTTP client για αρχές χρονοσφράγισης RFC 3161. Η βιβλιοθήκη υπολογίζει το hash που πρέπει να υποβληθεί και ενσωματώνει ξανά το εμπλουτισμένο CMS μόλις επιστρέψει το token, αλλά η δικτυακή επικοινωνία με τη TSA είναι δική σας δουλειά. Και τα δύο τυλίγονται εύκολα και είναι πραγματικά δυσάρεστο να διαπιστωθεί ότι λείπουν την εβδομάδα πριν από μια κυκλοφορία, οπότε σχεδιάστε τα από το πρώτο σκίτσο
Ένα ερώτημα για τη συμμόρφωση αξίζει να ξεκαθαριστεί ρητά, γιατί καθορίζει πού μπαίνει η τελευταία πύλη: χαλάει το PDF/A η προσθήκη υπογραφής; Όχι από μόνη της. Η υπογραφή φτάνει ως incremental update, και το ISO 19005-2 και έπειτα επιτρέπει ρητά υπογεγραμμένα έγγραφα. Το αγκάθι είναι η εμφάνιση της υπογραφής, που παίζει με τους ίδιους κανόνες όπως κάθε άλλο περιεχόμενο σελίδας, συμπεριλαμβανομένων των ενσωματωμένων γραμματοσειρών και της απουσίας χρώματος εξαρτώμενου από συσκευή. Έτσι η τελευταία πύλη στο workbench είναι άλλη μία εκτέλεση preflight, αυτή τη φορά πάνω στην υπογεγραμμένη έξοδο. Αντιμετωπίστε το CheckFileCompliance ως τον γρήγορο έλεγχο μέσα στη ροή και επαληθεύστε παρ' όλα αυτά τα release candidates με ανεξάρτητο εργαλείο όπως το veraPDF, αφού οι validators υλοποιούν επικαλυπτόμενα αλλά όχι πανομοιότυπα σύνολα κανόνων· όταν τα δύο διαφωνούν, το κείμενο του ευρήματος συνήθως κατονομάζει τη ρήτρα που πρέπει να διαβάσετε
Ένα σημείο σειράς προκύπτει από όλα αυτά. Η υπογραφή και η χρονοσφράγιση δεν είναι ένα ενιαίο πέρασμα: πρώτα γράφεται η baseline υπογραφή και ύστερα μια ξεχωριστή διεργασία χρονοσφράγισης εμπλουτίζει το CMS μέσα στον δεσμευμένο χώρο /Contents, και ακριβώς γι' αυτό η γραμμή με τα reserve bytes παραπάνω είχε τόσο βάρος. Για τα επίπεδα χρονοσφράγισης και μακροχρόνιας επικύρωσης που χτίζονται πάνω σε αυτό το workbench, ο αναλυτικός οδηγός υπογραφής και επικύρωσης PAdES οδηγεί την υπογραφή από baseline σε B-LT, ενώ το κομμάτι του preflight εμβαθύνει στον οδηγό preflight PDF/A και PDF/UA. Πλήρης τεκμηρίωση API και δοκιμαστικές λήψεις βρίσκονται στη σελίδα προϊόντος PDF Library for Delphi