Το PDFium Component υπογράφει ένα PDF με ψηφιακή υπογραφή PAdES B-B μέσω της μεθόδου του SignPades: φορτώνει το έγγραφο, κατακερματίζει (hashes) το υπογεγραμμένο εύρος byte, χτίζει μια δομή CAdES CMS, και προσαρτά την υπογραφή ως σταδιακή ενημέρωση. Το κρυπτογραφικό backend είναι μόνο για Windows, οπότε προστατεύστε κάθε κλήση με το PadesCryptoAvailable προτού υπογράψετε
Η κατάσταση είναι οικεία. Ένα συμβόλαιο PDF φτάνει στο γραφείο σας, το νομικό τμήμα το θέλει ψηφιακά υπογεγραμμένο προτού σταλεί, και αναζητάτε την ίδια έκδοση PDFium που χρησιμοποιείτε ήδη για να αποδώσετε και να επιθεωρήσετε έγγραφα, μόνο και μόνο για να ανακαλύψετε ότι το PDFium δεν μπορεί να γράψει καθόλου υπογραφή. Το API υπογραφής του είναι αυστηρά μόνο για ανάγνωση. Το PDFium Component κλείνει αυτό το κενό αναλαμβάνοντας ολόκληρη τη ροή εργασίας υπογραφής σε Pascal, από τη συνάρτηση κατακερματισμού (hash function) έως την εισαγωγή σε επίπεδο byte, και αυτό το άρθρο περιγράφει αυτήν τη ροή από άκρη σε άκρη
Γιατί το PDFium δεν μπορεί να γράψει ψηφιακή υπογραφή;
Το PDFium εκθέτει τις υπογραφές ως αντικείμενα μόνο για ανάγνωση και δεν προσφέρει τίποτα που να δημιουργεί μία. Η οικογένεια FPDFSignatureObj_* σας επιτρέπει να απαριθμήσετε μια υπάρχουσα υπογραφή, να διαβάσετε το /Contents της και να επιθεωρήσετε το /ByteRange της, αλλά δεν υπάρχει αντίστοιχο που να χτίζει ένα λεξικό υπογραφής, να δεσμεύει μια θέση /Contents ή να γράφει ένα εύρος byte. Η σταδιακή αποθήκευση υπάρχει (FPDF_SaveAsCopy με FPDF_INCREMENTAL) αλλά δεν φέρει κάποιο γάντζο υπογραφής (signing hook). Οποιοδήποτε στοιχείο υπογράφει ένα PDF πάνω από το PDFium πρέπει επομένως να παράγει κάθε byte της υπογραφής μόνο του, γι' αυτό και το PDFium Component χτίζει τον μηχανισμό από τρεις μονάδες (units) καθαρής Pascal. Ο FPC 3.2.2 παρέχει md5 και sha1 αλλά καθόλου SHA-2, και το API SHA-256 του Delphi System.Hash δεν είναι συμβατό σε επίπεδο κώδικα με τον FPC, οπότε το FPdfSha256 είναι μια αυτόνομη υλοποίηση FIPS 180-4 που διατηρεί κάθε διαδρομή κώδικα CMS σε έναν τύπο TSHA256Digest χωρίς διακλαδώσεις μεταγλωττιστή. Το FPdfAsn1 παρέχει τον κωδικοποιητή και αναγνώστη DER που χρειάζονται οι δομές CMS, και το FPdfCms συναρμολογεί το CAdES SignedData πάνω και στα δύο
Πώς υπογράφετε ψηφιακά ένα PDF στο Delphi;
Φορτώστε το έγγραφο, και στη συνέχεια καλέστε το SignPades με ένα αποτύπωμα πιστοποιητικού (certificate thumbprint). Το PDFium Component επιλύει αυτό το αποτύπωμα έναντι του αποθετηρίου πιστοποιητικών "MY" του Τρέχοντος Χρήστη, ανακτά το αντίστοιχο πιστοποιητικό και το ιδιωτικό του κλειδί, και γράφει ένα υπογεγραμμένο αντίγραφο στη διαδρομή που ορίζετε
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('η υπογραφή PAdES απαιτεί το Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // το έγγραφο προς υπογραφή
Pdf.Active := True;
// Δεύτερο όρισμα: αποτύπωμα SHA-1 ενός πιστοποιητικού στο αποθετήριο "MY" του Τρέχοντος Χρήστη. Πρώτο όρισμα: προορισμός για το υπογεγραμμένο αντίγραφο.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Η υπογραφή απέτυχε');
finally
Pdf.Free;
end;
end;
Το PadesCryptoAvailable είναι η δοκιμή (probe) που ελέγχετε πάντα πρώτη. Στα Windows επιστρέφει True και το backend crypt32/ncrypt είναι ενεργό. Σε οποιαδήποτε άλλη πλατφόρμα επιστρέφει False και μια κλήση υπογραφής θα εγείρει σφάλμα EPadesCrypto. Η αντιμετώπιση αυτής της προστασίας ως υποχρεωτικής αποτρέπει την αποτυχία ενός build για Linux ή macOS κατά το χρόνο εκτέλεσης σε μια διαδρομή που δεν μπορεί να λειτουργήσει εκεί. Το ίδιο το αποτύπωμα είναι το hash SHA-1 του πιστοποιητικού, η ίδια τιμή που δείχνει ο διαχειριστής πιστοποιητικών των Windows στην καρτέλα Λεπτομέρειες, και ονομάζει έναν συγκεκριμένο υπογράφοντα χωρίς να τοποθετεί ποτέ υλικό κλειδιού στον κώδικά σας
Tι μπαίνει στο CMS: υπογεγραμμένα χαρακτηριστικά (signed attributes) και RFC 5652
Μια υπογραφή αναφοράς PAdES δεν είναι μια απλή υπογραφή RSA πάνω στο αρχείο. Είναι μια δομή CMS SignedData του CAdES που μεταφέρει ένα υποχρεωτικό σύνολο υπογεγραμμένων χαρακτηριστικών, και το FPdfCms.BuildSignedData εκπέμπει ακριβώς αυτό το σύνολο: content-type, message-digest, και signing-certificate-v2, το χαρακτηριστικό ESS που συνδέει την υπογραφή με το πιστοποιητικό του υπογράφοντος μέσω hash. Μια λεπτομέρεια εδώ ανατρέπει σχεδόν κάθε χειροποίητη υλοποίηση CMS. Το RFC 5652 §5.4 απαιτεί η σύνοψη των υπογεγραμμένων χαρακτηριστικών να υπολογίζεται πάνω στην κωδικοποίηση DER SET OF, ετικέτα 0x31, ενώ τα ίδια χαρακτηριστικά ταξιδεύουν μέσα στο SignerInfo κάτω από την ετικέτα IMPLICIT [0], 0xA0. Το PDFium Component κωδικοποιεί το σύνολο χαρακτηριστικών μία φορά, υπολογίζει τη σύνοψη της μορφής 0x31, και στη συνέχεια ξαναγράφει μόνο το αρχικό byte της ετικέτας σε 0xA0 για εκπομπή, έτσι ώστε ένα buffer να εξυπηρετεί και τους δύο ρόλους χωρίς δεύτερο πέρασμα πάνω από το δέντρο
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // υπογράφων στο αποθετήριο MY
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // δεκαεξαδικό πλάτος του /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Η υπογραφή απέτυχε');
finally
Pdf.Free;
end;
end;
Η υπερφόρτωση επιλογών προσθέτει τα μεταδεδομένα του λεξικού υπογραφής που ορίζει το ISO 32000-1 §12.8.1: Reason, Location, ContactInfo, και Name, όλα προαιρετικά και όλα γραμμένα στο λεξικό τιμής υπογραφής. Ένας περιορισμός είναι εύκολο να παραβιαστεί. Εάν ορίσετε το CommitmentTypeOid για να προσθέσετε ένα υπογεγραμμένο χαρακτηριστικό CAdES commitment-type-indication, μην ορίσετε επίσης το Reason. Το ETSI EN 319 142-1 §6.3 απαγορεύει τη μεταφορά και των δύο, επειδή τα δύο εκφράζουν την ίδια πρόθεση με διαφορετικά μέσα
Πώς ταιριάζουν το ByteRange και η θέση /Contents;
Μια υπογραφή πρέπει να καλύπτει ολόκληρο το αρχείο εκτός από τα bytes που κρατούν την ίδια την υπογραφή, και το PAdES επιλύει αυτήν την κυκλικότητα με ένα placeholder σταθερού πλάτους που το SignPadesBytes διαχειρίζεται με ακρίβεια. Δεσμεύει μια δεκαεξαδική συμβολοσειρά /Contents μεγέθους ContentsSize bytes (16384 από προεπιλογή, άνετα μεγαλύτερη από ένα τυπικό CMS SignedData), σειριοποιεί τη σταδιακή ενημέρωση για να εντοπίσει την ακριβή μετατόπιση της θέσης, και στη συνέχεια υπολογίζει το /ByteRange ως δύο διαστήματα που περιβάλλουν τη θέση — οτιδήποτε πριν από τον οριοθέτη ανοίγματος της δεκαεξαδικής συμβολοσειράς και οτιδήποτε μετά τον οριοθέτη κλεισίματος. Το SHA-256 εκτελείται μόνο πάνω σε αυτά τα δύο διαστήματα. Το ολοκληρωμένο CMS κωδικοποιείται σε δεκαεξαδικό στη δεσμευμένη θέση, γεμίζει με μηδενικά (zero-padded) μέχρι το σταθερό πλάτος, και προσαρτάται η ενημέρωση παραπομπής. Επειδή το πλάτος καθορίζεται εκ των προτέρων, το γέμισμα της θέσης δεν μετατοπίζει κανένα byte προς τα κάτω, γεγονός που αποτελεί το λόγο για τον οποίο το εύρος byte παραμένει έγκυρο. Τα αρχικά bytes του εγγράφου διατηρούνται κατά λέξη, οπότε μια προηγούμενη υπογραφή στο ίδιο αρχείο επιβιώνει άθικτη ακριβώς όπως απαιτεί η σταδιακή υπογραφή του ISO 32000-1 §12.8.1
Tο Windows CNG backend και τα όριά του
Το PDFium Component υπογράφει μόνο στα Windows, και αυτό το όριο είναι σκόπιμο. Το FPdfCryptoWin συνδέει τα crypt32.dll και ncrypt.dll δυναμικά, χωρίς να προσθέτει εξάρτηση DLL κατά το χρόνο μεταγλώττισης, και η αλυσίδα υπογραφής είναι η τυπική CNG: ανοίγει την αποθήκη MY, βρίσκει το πιστοποιητικό με βάση το hash, αποκτά τη λαβή του ιδιωτικού κλειδιού του μέσω της CryptAcquireCertificatePrivateKey, και καλεί την NCryptSignHash. RSA με PKCS#1 v1.5, RSA-PSS, και ECDSA υποστηρίζονται όλα. Το ECDSA χρειάζεται μια διόρθωση που τα άλλα δεν χρειάζονται, επειδή η NCryptSignHash επιστρέφει το ακατέργαστο ζεύγος r-και-s κατά IEEE P1363, ενώ το CMS αναμένει μια SEQUENCE DER ECDSA-Sig-Value, οπότε το backend το επανακωδικοποιεί σύμφωνα με το RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // δεν υπάρχει backend υπογραφής σε αυτήν την πλατφόρμα
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
Η πρακτική συνέπεια είναι ότι το ιδιωτικό κλειδί πρέπει να βρίσκεται στο αποθετήριο πιστοποιητικών των Windows. Ένα πιστοποιητικό που κρατείται σε αρχείο PFX λειτουργεί μόνο αφού το εισαγάγετε στην αποθήκη του Τρέχοντος Χρήστη, οπότε το αποτύπωμά του είναι η τιμή που περνάτε στο SignPades. Αυτή η έκδοση δεν διαθέτει διαδρομή PKCS#11 ή HSM και κανένα backend αρχείου κλειδιού λογισμικού, οπότε όταν το PadesCryptoAvailable επιστρέφει False, απλώς δεν είναι δυνατή η υπογραφή σε αυτό το μηχάνημα
Πού σταματά το PAdES B-B
Το PAdES B-B είναι η βάση, το κατώτατο όριο των τεσσάρων επιπέδων PAdES: αποδεικνύει ποιος υπέγραψε και ότι τα bytes δεν έχουν αλλάξει έκτοτε, και τίποτα πέρα από αυτό. Μια υπογραφή B-B δεν φέρει αξιόπιστη χρονοσήμανση, οπότε δεν μπορεί να αποδείξει πότε έγινε η υπογραφή, και δεν ενσωματώνει δεδομένα ανάκλησης, οπότε ένας ελεγκτής χρόνια αργότερα πρέπει να ανακτήσει ο ίδιος την αλυσίδα πιστοποιητικών και την κατάστασή της. Αυτά τα κενά είναι ακριβώς εκείνα που κλείνουν τα υψηλότερα επίπεδα. Όταν χρειάζεστε χρόνο υπογραφής που θα αποδεχτεί ένας ελεγκτής, η προσθήκη χρονοσήμανσης RFC 3161 και DSS για μακροπρόθεσμη επικύρωση μεταφέρει την υπογραφή στο B-T και άνω. Όταν θέλετε να διαβάσετε μια έτοιμη υπογραφή και να επιβεβαιώσετε ποιο επίπεδο έφτασε, ο έλεγχος υπογραφής PDF και του επιπέδου PAdES είναι το συνοδευτικό εργαλείο. Και προτού υπογράψετε οτιδήποτε, ο έλεγχος κινδύνων ασφαλείας PDF σας λέει τι πρόκειται να υπογράψετε
Οι μέθοδοι SignPades που εμφανίζονται εδώ παρέχονται με το PDFium Component για Delphi και C++Builder, μαζί με τον έλεγχο υπογραφής μόνο για ανάγνωση που παρέχει το PDFium έτοιμο προς χρήση