Ένα υπογεγραμμένο PDF που άλλαξε μετά την υπογραφή δεν είναι αυτόματα κατεστραμμένο. Το ISO 32000-1 επιτρέπει αυξητικές ενημερώσεις πάνω σε μια υπογραφή, και μόνο μερικές από αυτές παραβιάζουν την πολιτική που όρισε ο υπογράφων. Το HotPDF Component για Delphi και C++Builder απαντά σε αυτό το ερώτημα με την AnalyzeLoadedSignatureRevisions, η οποία ταξινομεί κάθε αναθεώρηση μετά την υπογραφή και τη βαθμολογεί έναντι DocMDP και FieldMDP. Το σενάριο είναι οικείο σε όποιον διανέμει λογισμικό συμβολαίων: ο πελάτης σας υπογράφει μια σύμβαση αγοράς, τη στέλνει, και την παίρνει πίσω με μια σελίδα παραρτήματος επισυναπτόμενη. Ο reader εμφανίζει κίτρινη γραμμή που λέει ότι η υπογραφή είναι άθικτη αλλά το έγγραφο έχει αλλάξει από τότε που υπογράφηκε, και κανείς στο δωμάτιο δεν μπορεί να πει αν αυτό είναι μια κανονική ροή αντι-υπογραφής ή κάποιος που επεξεργάζεται ήσυχα ένα υπογεγραμμένο συμβόλαιο
Τι μετρά ως νόμιμη αλλαγή μετά την υπογραφή;
Μια αλλαγή είναι νόμιμη όταν η σημασιολογική κατηγορία της πέφτει μέσα στην άδεια που δήλωσε η πιστοποιητική υπογραφή. Το ISO 32000-1 §12.8.2.2 ορίζει τον μετασχηματισμό DocMDP με τιμή /P 1, 2 ή 3: το 1 δεν επιτρέπει καμία αλλαγή, το 2 επιτρέπει συμπλήρωση φόρμας και υπογραφή, το 3 επιτρέπει συμπλήρωση φόρμας, υπογραφή και σημειώσεις. Το HotPDF τα εκθέτει ως τιμές THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign και dmpFormFillSignAndAnnotate, με το dmpNone κρατημένο για αποτελέσματα επιθεώρησης που δεν φέρουν καθόλου μετασχηματισμό DocMDP
Οι κατηγορίες είναι διατεταγμένες, και αυτή η διάταξη είναι η μηχανή ολόκληρου του ελέγχου. Το THPDFRevisionModificationLevel τρέχει rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, σκόπιμα διαταγμένο έτσι ώστε ένας μεγαλύτερος τακτικός αριθμός να μην είναι ποτέ λιγότερο περιοριστικός. Ολόκληρο το έγγραφο ανάγεται στο μέγιστο επίπεδο που παρατηρήθηκε σε όλες τις αναθεωρήσεις μετά την υπογραφή, και η σύγκριση DocMDP γίνεται ένας μόνο ακέραιος έλεγχος. Μια απόχρωση έχει σημασία νωρίς: στο dmpNoChanges η ανάλυση εξακολουθεί να αποδέχεται το rmlLongTermValidation. Η προσθήκη υλικού επικύρωσης DSS και VRI ή μιας χρονοσφραγίδας εγγράφου σε ένα πιστοποιημένο αρχείο είναι συντήρηση της υπογραφής, όχι τροποποίηση του εγγράφου, και η αντιμετώπισή της ως παραβίαση θα έσπαγε κάθε ροή εργασίας μακροχρόνιας αρχειοθέτησης που υπάρχει
Πώς ανακατασκευάζει το HotPDF την αλυσίδα αναθεωρήσεων;
Δομικά, όχι ευρετικά. Σύμφωνα με το ISO 32000-1 §7.5.6 μια αυξητική ενημέρωση προσαρτά ένα νέο τμήμα cross-reference του οποίου το /Prev δείχνει στο προηγούμενο, οπότε το HotPDF διαβάζει το startxref από την ουρά, αναλύει το τμήμα εκεί, ακολουθεί το /Prev προς τα πίσω και επαναλαμβάνει, επιστρέφοντας τα τμήματα από το παλαιότερο πρώτα. Δύο όρια ασφαλείας κάθονται σε αυτόν τον βρόχο και αξίζει να τα γνωρίζετε και τα δύο όταν διαλογετε ένα αρχείο που αποτυγχάνει: ένα /Prev που δείχνει σε μια θέση ήδη επισκεφθείσα τερματίζει τη διαδρομή με ρητή διάγνωση κύκλου αντί να στριφογυρίζει, και μια αλυσίδα μεγαλύτερη από χίλιες αναθεωρήσεις απορρίπτεται εντελώς. Και τα δύο εμφανίζονται στο Analysis.Issue με τη συνάρτηση να επιστρέφει False, και κανένα από τα δύο δεν πρέπει να καλυφθεί, επειδή ένα κυκλικό /Prev είναι ένα κακοσχηματισμένο ή εχθρικό αρχείο και όχι απλώς ασυνήθιστο
Τέσσερα ιστορικά σχήματα εμφανίζονται σε πραγματικά έγγραφα και και τα τέσσερα υποστηρίζονται: παραδοσιακοί πίνακες xref αναλυμένοι γραμμή προς γραμμή, cross-reference streams αποσυμπιεσμένα και αποκωδικοποιημένα μέσω των πεδίων /W και /Index τους, αρχεία υβριδικής αναφοράς των οποίων ο παραδοσιακός trailer φέρει ένα κλειδί /XRefStm που αναλύεται και συγχωνεύεται στην ίδια αναθεώρηση (η περίπτωση παραγωγού Office, που καλύπτεται στο άρθρο για τα υβριδικά cross-reference streams), και αντικείμενα που ζουν μέσα σε ένα container ObjStm, τα οποία έχουν σημασία επειδή μια σύγχρονη ενημέρωση συνήθως βάζει το αλλαγμένο λεξικό σε ένα συμπιεσμένο stream αντί να το γράψει απευθείας, όπως περιγράφεται στο κείμενο για τα object streams και τις αυξητικές ενημερώσεις. Η υπογραφή αγκυρώνει τη διαίρεση: το /ByteRange[2] + /ByteRange[3] γίνεται SignedRevisionLength, και κάθε τμήμα σε αυτή τη θέση ή πέρα από αυτήν είναι μετά την υπογραφή. Το αν το εύρος byte εξακολουθεί να κερματίζεται σωστά είναι ξεχωριστό ερώτημα, που απαντάται από την VerifyLoadedSignature και καλύπτεται στο άρθρο για την επαλήθευση ψηφιακών υπογραφών PDF
Πώς ταξινομείται κάθε αλλαγμένο αντικείμενο
Η ταξινόμηση τρέχει ανά αντικείμενο, και μετά διαδίδεται κατά μήκος αναφορών. Για κάθε αριθμό αντικειμένου που αγγίζει ένα τμήμα μετά την υπογραφή, το HotPDF διαβάζει το νέο σώμα και το σώμα όπως στεκόταν στην υπογεγραμμένη στιγμιαία εικόνα· ένα πανομοιότυπο σώμα είναι rmlNone, επειδή οι παραγωγοί όντως επανεγγράφουν αντικείμενα χωρίς να τα αλλάζουν. Οι αναγνωριστές είναι στενοί σκόπιμα. Ένα αντικείμενο /Type /DocTimeStamp, ή ένα του οποίου το /SubFilter είναι ETSI.RFC3161, είναι rmlLongTermValidation, όπως και οτιδήποτε προσβάσιμο από το δέντρο /DSS του καταλόγου· ένα λεξικό /Type /Sig είναι rmlFormFillAndSign. Για containers ο έλεγχος είναι ποια κλειδιά μετακινήθηκαν, όχι τι είναι το αντικείμενο: ο κατάλογος μπορεί μόνο να αποκτήσει ή να αλλάξει τα /DSS, /Extensions ή /AcroForm· το λεξικό AcroForm μόνο τα /Fields, /SigFlags, /NeedAppearances, /DR, /DA ή /Q· μια σελίδα μόνο το /Annots· ένα πεδίο ή widget μόνο τα /V, /AP, /AS ή /M. Οτιδήποτε εκτός αυτών των συνόλων πέφτει σε rmlOther, που είναι ακριβώς πώς πιάνεται η προσαρτημένη σελίδα παραρτήματος: η προσθήκη σελίδας αναδιατάσσει το δέντρο σελίδων με τρόπους που καμία λίστα αποδεκτών δεν καλύπτει, και καμία ποσότητα νόμιμης συμπλήρωσης φόρμας δεν της μοιάζει
Μετά τα επίπεδα διαδίδονται, με κάθε container να κληρονομεί το μέγιστο επίπεδο των αλλαγμένων παιδιών στα οποία δείχνει, επαναλαμβανόμενο μέχρι η ανάθεση να σταθεροποιηθεί. Αυτό είναι που κάνει τα appearance streams να λειτουργούν. Ένα συμπληρωμένο πεδίο κειμένου επανεγγράφει το /V και δείχνει σε ένα φρέσκο stream /AP, και αυτό το stream από μόνο του είναι ένα ανώνυμο blob τελεστών περιεχομένου χωρίς τύπο να αναγνωριστεί· επειδή το πεδίο που το κατέχει είναι rmlFormFillAndSign, το stream κληρονομεί το ίδιο επίπεδο αντί να πέσει σε rmlOther. Η ίδια διάδοση μεταφέρει το πλαίσιο DSS σε streams πιστοποιητικών και ανάκλησης που διαφορετικά θα ήταν αταξινόμητα
Γιατί ένα αδιάβαστο αντικείμενο μετρά ως παραβίαση;
Επειδή η εναλλακτική είναι ένας validator που ξεπερνιέται γράφοντας κάτι που δεν καταλαβαίνει. Τρεις καταστάσεις καταλήγουν στο rmlOther χωρίς προσφυγή στο HotPDF: ένα αντικείμενο του οποίου το σώμα δεν μπόρεσε να διαβαστεί από την αναθεώρηση, ένα αντικείμενο που η αναθεώρηση σημαδεύει ως ελεύθερο, και ένα αντικείμενο που δεν ταιριάζει με κανέναν από τους παραπάνω αναγνωριστές. Κάθε ένα καταγράφει μια συγκεκριμένη διάγνωση στο πεδίο Issue της αναθεώρησης, ώστε ένας χειριστής να μπορεί να δει ποιος αριθμός αντικειμένου παρήγαγε την ετυμηγορία
Η ελευθέρωση είναι η πιο κοφτερή από τις τρεις. Μια αναθεώρηση μετά την υπογραφή που σημαδεύει ένα προηγουμένως ορισμένο αντικείμενο ως ελεύθερο έχει διαγράψει περιεχόμενο από ένα υπογεγραμμένο έγγραφο, και κανένα επίπεδο άδειας κάτω από το §12.8.2.2 δεν το επιτρέπει· οι αριθμοί αντικειμένων καταλήγουν στο FreedObjectNumbers και η αναθεώρηση ανεβαίνει σε rmlOther. Τα αδιάβαστα αντικείμενα ακολουθούν την ίδια λογική για διαφορετικό λόγο. Ένας validator που δεν μπορεί να αναλύσει ένα αντικείμενο δεν έχει βάση να το χαρακτηρίσει ακίνδυνο, και η ειλικρινής απάντηση σε αυτό δεν είναι σιωπή. Η αναφορά μιας ασυνήθιστης αλλά αβλαβούς κατασκευής ως παραβίασης κοστίζει μια ανθρώπινη επιθεώρηση· το αντίθετο λάθος στέλνει ένα υπογεγραμμένο συμβόλαιο με μια απαρατήρητη επεξεργασία μέσα του
Ανάγνωση της ετυμηγορίας σε Delphi
Η κλήση είναι σύντομη. Φορτώστε το έγγραφο, επιλέξτε έναν δείκτη υπογραφής, διαβάστε την εγγραφή· η υπερφόρτωση χωρίς παραμέτρους ξανανοίγει το αρχείο από το οποίο φορτώθηκε το έγγραφο, και η υπερφόρτωση TStream παίρνει bytes παρεχόμενα από τον καλούντα και επαναφέρει τη θέση του stream πριν επιστρέψει. Το PolicyCompliant είναι το μοναδικό boolean που θέλουν οι περισσότεροι καλούντες, συνδυάζοντας τρεις ανεξάρτητες αποφάσεις: τη δομική εγκυρότητα των λεξικών αδειών, το DocMDPCompliant, και το FieldMDPCompliant. Κρατήστε τα συστατικά ορατά στο UI σας αντί να τα συμπτύσσετε, και σημειώστε ότι ένα έγγραφο χωρίς μετασχηματισμό DocMDP αφήνει το DocMDPCompliant στο True, αφού μια συνηθισμένη υπογραφή έγκρισης δεν δηλώνει καμία πολιτική προς παραβίαση και το συγκεντρωτικό ModificationLevel γίνεται τότε περιγραφικό αντί για ετυμηγορία
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Για διαλογή συνήθως θέλετε την ανάλυση ανά αναθεώρηση αντί για τη σύνοψη, επειδή λέει πότε στο ιστορικό του εγγράφου κάτι πήγε στραβά. Κάθε καταχώριση στο Analysis.Revisions φέρει τον δείκτη της στην αλυσίδα, τη θέση cross-reference στην οποία γράφτηκε, το δικό της επίπεδο τροποποίησης, και τους αριθμούς αντικειμένων που εμπλέκονται
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
Το FieldMDP κρίνεται ξεχωριστά, και αυτό είναι σκόπιμο
Ένα έγγραφο μπορεί να ικανοποιεί το DocMDP και να παραμένει παράνομο, γι' αυτό το FieldMDPCompliant είναι ξεχωριστό boolean αντί να είναι συμπτυγμένο στη σύγκριση επιπέδων. Το ISO 32000-1 §12.8.2.4 ορίζει τον μετασχηματισμό FieldMDP, και το §12.7.5.5 τη σχετική καταχώριση /SigFieldLock, για να παγώσουν ονομασμένα πεδία φόρμας τη στιγμή της υπογραφής ακόμη κι όταν το έγγραφο συνολικά εξακολουθεί να επιτρέπει συμπλήρωση φόρμας. Η συμπλήρωση ενός πεδίου είναι ενέργεια επιπέδου 2· η συμπλήρωση ενός πεδίου που κλείδωσε ο υπογράφων είναι παραβίαση ανεξάρτητα από επίπεδο. Το HotPDF διαβάζει το εύρος στο THPDFFieldLockAction ως flaAll, flaInclude ή flaExclude, με flaNone για αποτελέσματα που δεν φέρουν καμία πολιτική κλειδώματος, και τα ονόματα στο Permissions.FieldNames: το flaAll κλειδώνει τα πάντα, το flaInclude κλειδώνει τα ονόματα στη λίστα, το flaExclude κλειδώνει τα πάντα εκτός από αυτά. Μια λεπτομέρεια έχει σημασία στην ανάγνωση των αποτελεσμάτων, καθώς μόνο πεδία ήδη παρόντα στην υπογεγραμμένη στιγμιαία εικόνα αναφέρονται στο ChangedFieldNames, επειδή ένα πεδίο που δημιουργήθηκε εντελώς μετά την υπογραφή δεν έχει υπογεγραμμένη κατάσταση να έρθει σε αντίφαση και πιάνεται από τη διαδρομή DocMDP αντ' αυτού
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
Τι δεν θα σας πει αυτή η ανάλυση
Δεν επαληθεύει μια υπογραφή. Η AnalyzeLoadedSignatureRevisions συλλογίζεται πάνω σε δομή και άδειες· το αν το υπογεγραμμένο εύρος byte εξακολουθεί να κερματίζεται στην τιμή στο CMS blob, και αν η αλυσίδα πιστοποιητικού του υπογράφοντος οδηγεί σε κάτι που εμπιστεύεστε, απαντώνται από τις VerifyLoadedSignature και VerifyLoadedSignatureWithTrust. Ένα αρχείο μπορεί να είναι απόλυτα συμμορφούμενο με την πολιτική και κρυπτογραφικά άχρηστο, οπότε οι δύο έλεγχοι ανήκουν πλάι πλάι σε κάθε πραγματική πύλη αποδοχής. Επίσης δεν διαβάζει πρόθεση μέσα σε ρεύματα περιεχομένου: μια σελίδα της οποίας το ρεύμα περιεχομένου αντικαταστάθηκε ολόκληρα πιάνεται ως αλλαγή εκτός λίστας αποδεκτών, αλλά η ανάλυση δεν θα σας πει ότι η αντικατάσταση άλλαξε ένα ποσό πληρωμής. Μια ετυμηγορία rmlOther σημαίνει ότι ένας άνθρωπος πρέπει να κοιτάξει, όχι ότι έγινε απάτη, και μια συμμορφούμενη ετυμηγορία σημαίνει ότι η αλλαγή ταιριάζει σε επιτρεπόμενη κατηγορία, όχι ότι η αλλαγή ήταν επιθυμητή. Όταν το μόνο που χρειάζεστε είναι αυτό που δήλωσε ο υπογράφων, χωρίς τη διαδρομή αναθεωρήσεων, η GetLoadedSignaturePermissions επιστρέφει τα λεξικά πολιτικής μόνα τους
Όλα όσα περιγράφονται εδώ τρέχουν εγγενώς σε Delphi και C++Builder χωρίς εξωτερική υπηρεσία υπογραφής στη ροή, που είναι αυτό που το κάνει πρακτικό να τρέχει σε κάθε εισερχόμενο έγγραφο αντί μόνο σε αυτά που κάποιος ήδη υποψιαζόταν. Το πλήρες API υπογραφών και αναθεωρήσεων, συμπεριλαμβανομένων των μεθόδων αδειών και επαλήθευσης με τις οποίες συνδυάζεται, είναι μέρος του HotPDF Component για Delphi και C++Builder