Τεχνικό Άρθρο

Έλεγχος Ψηφιακών Υπογραφών PDF και Επιπέδων PAdES στο Delphi

Έχετε λάβει ένα υπογεγραμμένο PDF και πρέπει να εμφανίσετε, στο πρόγραμμα προβολής σας, ποιος το υπέγραψε, πότε υπογράφηκε, αν η υπογραφή καλύπτει ολόκληρο το αρχείο και πόσο κοντά βρίσκεται στη μακροπρόθεσμη συμμόρφωση. Το PDFium Component για Delphi και Lazarus απαντά και στις τέσσερις ερωτήσεις με κλήσεις μόνο για ανάγνωση: τα FPDF_GetSignatureCount και η οικογένεια FPDFSignatureObj_* εκθέτουν το λεξικό της υπογραφής, και το TPdf.ValidatePades ταξινομεί το επίπεδο αναφοράς PAdES. Αυτό είναι το πρώτο από τα τρία άρθρα σχετικά με τις υπογραφές PDF με το PDFium. Τα 2 που ακολουθούν καλύπτουν τη δημιουργία μιας υπογραφής B-B και την προσθήκη μακροπρόθεσμων χρονοσημάνσεων. Ωστόσο, ένα όριο πρέπει να τεθεί εκ των προτέρων: τα πάντα εδώ αφορούν την επιθεώρηση, και η ανάγνωση των ισχυρισμών μιας υπογραφής είναι διαφορετική εργασία από την επαλήθευση της κρυπτογράφησής της ή την απόφαση για το αν εμπιστεύεστε τον υπογράφοντα

Γιατί ένα λεξικό υπογραφής PDF δεν είναι απλώς ένα blob από bytes

Μια υπογραφή PDF είναι ένα λεξικό (dictionary), όχι ένα αδιαφανές συνημμένο, και οι δύο πιο σημαντικές καταχωρίσεις του σας λένε πόσο μεγάλο μέρος του αρχείου προστατεύεται στην πραγματικότητα. Το ISO 32000-1 §12.8 ορίζει το λεξικό υπογραφής με μια καταχώριση /ByteRange και μια καταχώριση /Contents. Το /Contents περιέχει μια δομή CMS SignedData κωδικοποιημένη σε δεκαεξαδικό (RFC 5652), τον κρυπτογραφικό φάκελο που μεταφέρει το πιστοποιητικό του υπογράφοντος, τα υπογεγραμμένα χαρακτηριστικά και την ίδια την τιμή της υπογραφής. Το /ByteRange είναι το μέρος που οι προγραμματιστές υποτιμούν: είναι ένας πίνακας δύο διαστημάτων μετατόπισης-μήκους (offset-length spans) που μαζί καλύπτουν ολόκληρο το αρχείο εκτός από τη δεκαεξαδική συμβολοσειρά /Contents. Αυτό το κενό είναι ακριβώς το σημείο όπου κάθονται τα bytes της υπογραφής, και τα δύο διαστήματα εκατέρωθεν είναι ακριβώς αυτά στα οποία δεσμεύεται η υπογραφή

Η σχεδίαση του ByteRange είναι αυτή που καθιστά μια σταδιακή αποθήκευση (incremental save) ελέγξιμη. Επειδή ένας υπογράφων δεν μπορεί να κάνει hash bytes υπογραφής που δεν υπάρχουν ακόμη, το αρχείο χωρίζεται γύρω από το placeholder /Contents και όλα τα άλλα γίνονται hashed στην υπογραφή. Μια υπογραφή της οποίας το ByteRange δεν φτάνει στο τέλος του αρχείου είναι προειδοποιητικό σημάδι: περιεχόμενο που προσαρτάται μετά το καλυπτόμενο εύρος, μέσω μεταγενέστερης σταδιακής ενημέρωσης, δεν θα έσπαγε την υπογραφή παρόλο που αλλάζει αυτό που βλέπει ο αναγνώστης. Έτσι, το πρώτο πράγμα που ελέγχει ένας σοβαρός επιθεωρητής δεν είναι το ποιος υπέγραψε, αλλά το αν η υπογραφή καλύπτει τα bytes που φαίνεται να προσυπογράφει

Ανάγνωση του λεξικού υπογραφής με το API μόνο για ανάγνωση του PDFium

Το PDFium Component εμφανίζει το λεξικό της υπογραφής μέσω δύο μελών μόνο για ανάγνωση: το SignatureCount και την εγγραφή Signature[Index]. Στο παρασκήνιο καλούν τα FPDF_GetSignatureCount, FPDF_GetSignatureObject, και τους accessors FPDFSignatureObj_* για τα /SubFilter, /ByteRange, /Contents, /Reason και το χρόνο υπογραφής. Το "μόνο για ανάγνωση" είναι η φράση-κλειδί εδώ: το PDFium μπορεί να απαριθμήσει και να διαβάσει υπογραφές αλλά δεν διαθέτει API για να δημιουργήσει ή να γράψει μία, γι' αυτό και το σκέλος της υπογραφής αυτής της σειράς υλοποιείται από την ίδια τη βιβλιοθήκη και όχι από το PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // συμβολοσειρά ημερομηνίας υπογράφοντος, π.χ. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // ακατέργαστο DER SignedData ληφθέν από το /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = χωρίς πιστοποίηση, 1..3 = επίπεδο MDP
    end;
  finally
    Pdf.Free;
  end;
end;

Κάθε εγγραφή TPdfSignature αντιστοιχίζει απευθείας στο λεξικό. Το Encoding είναι το /SubFilter, το πιο διαγνωστικό πεδίο, επειδή ονομάζει τον χειριστή της υπογραφής και διαχωρίζει αμέσως μια σύγχρονη υπογραφή ETSI.CAdES.detached signature από μια παλιά ή απαγορευμένη. Το Time είναι ο δηλωμένος από τον δημιουργό χρόνος υπογραφής ως συμβολοσειρά ημερομηνίας PDF, η οποία αποτελεί ισχυρισμό του υπογράφοντος και όχι αξιόπιστο χρόνο. Το Content είναι το ακατέργαστο CMS SignedData, και το Permission εκθέτει το επίπεδο πιστοποίησης DocMDP (0 για μια απλή υπογραφή έγκρισης, 1 έως 3 για μια υπογραφή πιστοποίησης που κλειδώνει τις μεταγενέστερες αλλαγές). Το μόνο πεδίο που δεν εμφανίζει η εγγραφή είναι το αναλυμένο ByteRange, και αυτή η παράλειψη είναι σκόπιμη, επειδή το ValidatePades κάνει την αριθμητική κάλυψης του ByteRange για εσάς αντί να σας αναγκάζει να την ξανακάνετε χειροκίνητα

Ποια είναι η διαφορά μεταξύ PAdES B-B, B-T, B-LT και B-LTA;

Τα τέσσερα επίπεδα αναφοράς PAdES σχηματίζουν μια σκάλα από μια ελάχιστα έγκυρη υπογραφή έως μια κατασκευασμένη για να επιβιώσει δεκαετίες αρχειοθέτησης, και κάθε επίπεδο περιέχει αυστηρά το επίπεδο που βρίσκεται κάτω από αυτό. Το ETSI EN 319 142-1 τα ορίζει ως B-B, B-T, B-LT και B-LTA. Το B-B (Basic) είναι η υπογραφή συν τα υποχρεωτικά υπογεγραμμένα χαρακτηριστικά και τίποτα περισσότερο. Το B-T (Timestamp) προσθέτει μια αξιόπιστη χρονοσήμανση RFC 3161 πάνω από την υπογραφή, οπότε η στιγμή της υπογραφής πιστοποιείται από μια αρχή χρονοσήμανσης αντί να δηλώνεται από το ρολόι του υπογράφοντος. Το B-LT (Long-Term) ενσωματώνει το υλικό επαλήθευσης — την αλυσίδα πιστοποιητικών και προαιρετικά τις αποκρίσεις OCSP ή CRL — μέσα στο αρχείο, έτσι ώστε η υπογραφή να μπορεί να επαληθευτεί χρόνια αργότερα όταν η υποδομή έκδοσης θα έχει χαθεί. Το B-LTA (Long-Term με Archive timestamp) τυλίγει μια χρονοσήμανση εγγράφου γύρω από αυτό το υλικό, προστατεύοντας τα ίδια τα μακροπρόθεσμα δεδομένα και δίνοντάς σας ένα σημείο για εκ νέου χρονοσήμανση προτού η υποκείμενη κρυπτογράφηση παλιώσει

Η πρακτική ανάγνωση αφορά τον χρονικό ορίζοντα. Μια υπογραφή B-B απαντά στο "αν υπέγραψε κάποιος αυτό το έγγραφο". Η B-T απαντά στο "και πότε, αποδεδειγμένα". Η B-LT απαντά στο "και αν μπορώ ακόμα να το ελέγξω αφού λήξουν τα πιστοποιητικά". Η B-LTA απαντά στο "και αν αυτός ο έλεγχος θα ισχύει ακόμα σε είκοσι χρόνια". Τα κανονιστικά προφίλ επιλέγουν ένα σκαλοπάτι: πολλά πλαίσια ηλεκτρονικής τιμολόγησης και eIDAS απαιτούν τουλάχιστον B-T, και οι εντολές αρχειοθέτησης απαιτούν B-LT ή B-LTA. Το να γνωρίζετε ποιο σκαλοπάτι φτάνει πραγματικά ένα έγγραφο, προτού το αποδεχτείτε ή το απορρίψετε, είναι ο σκοπός του βήματος επιθεώρησης

Ανίχνευση του επιπέδου αναφοράς με το TPdf.ValidatePades

Το PDFium Component μειώνει ολόκληρο το ζήτημα του επιπέδου σε μία μόνο κλήση. Το TPdf.ValidatePades επιστρέφει μια εγγραφή TPadesValidationResult της οποίας το πεδίο Level είναι ένα TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT ή plB_LTA — μαζί με ένα σύνολο προβλημάτων, έναν αριθμό υπογραφών και έναν αριθμό χρονοσημάνσεων εγγράφου. Το επίπεδο συνάγεται μονοτονικά: ο επικυρωτής καθορίζει πρώτα το B-B, στη συνέχεια προάγει σε B-T εάν υπάρχει χρονοσήμανση υπογραφής ή χρονοσήμανση εγγράφου, σε B-LT εάν ο κατάλογος φέρει ένα /DSS με πιστοποιητικά και τον δείκτη Level 1 /Extensions /ESIC, και σε B-LTA εάν μια χρονοσήμανση εγγράφου και ο δείκτης ESIC Level 2 είναι και οι δύο παρόντες. Δύο βοηθητικά στοιχεία καθιστούν το αποτέλεσμα αξιοποιήσιμο: το IsCompliant είναι True μόνο όταν το επίπεδο φτάνει τουλάχιστον το B-B και το σύνολο προβλημάτων είναι κενό, και το IsCompliantAt σας επιτρέπει να επιβάλλετε ένα ελάχιστο όριο πολιτικής όπως το plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B (basic)');
      plB_T:     Writeln('PAdES B-T (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Γιατί το adbe.pkcs7.sha1 είναι απαγορευμένο SubFilter;

Επειδή το SHA-1 είναι σπασμένο και ο χειριστής adbe.pkcs7.sha1 το ενσωματώνει. Αυτό το SubFilter προ-κατακερματίζει (pre-hashes) το έγγραφο με SHA-1 πριν το τυλίξει σε PKCS#7, και το SHA-1 είναι ευάλωτο σε συγκρούσεις (collisions) εδώ και χρόνια, οπότε η ρήτρα 6.3 του EN 319 142-1 το απαγορεύει ρητά για μια υπογραφή αναφοράς. Το ValidatePades εγείρει το ppeiForbiddenSubFilter όταν βλέπει adbe.pkcs7.sha1 ή adbe.x509.rsa_sha1, και εγείρει το ppeiBadDigestAlgorithm όταν το ίδιο το CMS χρησιμοποιεί MD5 ή SHA-1 ως σύνοψη μηνύματος (message digest) (ρήτρα 6.2.1). Αυτοί είναι δύο ξεχωριστοί έλεγχοι που εντοπίζουν την ίδια κατηγορία αδυναμίας σε δύο διαφορετικά επίπεδα

Το σύνολο των προβλημάτων έχει 26 μέλη συνολικά, και αυτά που θα συναντήσετε πιο συχνά συγκεντρώνονται γύρω από τη δομή και την κάλυψη. Το ppeiByteRangeNotCoveringFile είναι ο έλεγχος κάλυψης που περιγράφηκε νωρίτερα. Το ppeiForbiddenCertKey ενεργοποιείται όταν το λεξικό της υπογραφής φέρει μια καταχώριση /Cert, την οποία το PAdES απαγορεύει επειδή η αλυσίδα πρέπει να ζει μέσα στο CMS SignedData.certificates. Τα ppeiMissingSigningCertificate, ppeiMissingContentType και ppeiMissingMessageDigest επισημαίνουν υποχρεωτικά υπογεγραμμένα χαρακτηριστικά που απουσιάζουν, και το ppeiDetachedContentViolation εντοπίζει μια υπογραφή που εσφαλμένα ενσωματώνει το υπογεγραμμένο περιεχόμενο αντί να το αποσπά. Η απαρίθμηση του συνόλου μετατρέπει μια απλή απόρριψη σε διάγνωση που μπορείτε να καταγράψετε

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Τι δεν ελέγχει το ValidatePades

Το ValidatePades επικυρώνει τη δομή, όχι την εμπιστοσύνη, και η σύγχυση του ενός με το άλλο είναι επικίνδυνο σφάλμα. Ένα αποτέλεσμα plB_LTA σημαίνει ότι το έγγραφο περιέχει μια σωστά διαμορφωμένη υπογραφή B-LTA με όλα τα απαιτούμενα χαρακτηριστικά, υλικά και χρονοσημάνσεις στις σωστές θέσεις — δεν σημαίνει ότι η υπογραφή είναι κρυπτογραφικά έγκυρη, ότι η αλυσίδα πιστοποιητικών οδηγεί σε μια ρίζα που εμπιστεύεστε, ή ότι κανένα πιστοποιητικό στην αλυσίδα δεν έχει ανακληθεί. Ο επικυρωτής σκοπίμως δεν εκτελεί κρυπτογραφική επαλήθευση: δεν επανυπολογίζει την υπογραφή πάνω στο ByteRange, δεν χτίζει ούτε αξιολογεί την αλυσίδα εμπιστοσύνης και δεν ελέγχει την κατάσταση ανάκλησης OCSP ή CRL. Αυτός ο διαχωρισμός είναι σκόπιμος και χρήσιμος, επειδή η δομική επιθεώρηση είναι γρήγορη, πλήρως ντετερμινιστική και δεν απαιτεί κλειδιά, δίκτυο ή κρυπτογράφηση πλατφόρμας, οπότε το ValidatePades εκτελείται πανομοιότυπα σε Windows, Linux και macOS ως καθαρή Pascal πάνω στα bytes του αρχείου. Η επαλήθευση της αλυσίδας εμπιστοσύνης, αντίθετα, είναι αδιαχώριστη από την πολιτική — ποιες ρίζες εμπιστεύεστε, πώς ανακτάτε την ανάκληση, πόσο αυστηρή είναι η ανοχή σας στη χρονοσήμανση — και ανήκει σε μεταγενέστερο στάδιο που εξαρτάται από το αποθετήριο πιστοποιητικών της πλατφόρμας. Επομένως, αντιμετωπίστε ένα επιτυχές ValidatePades ως την απαραίτητη πύλη ότι μια υπογραφή είναι σωστά διαμορφωμένη, και στη συνέχεια παραδώστε μια δομικά υγιή υπογραφή σε πραγματική κρυπτογραφική επαλήθευση προτού βασιστείτε σε αυτήν

Αυτό το δομικό πέρασμα είναι το κατάλληλο σημείο για να ξεκινήσετε, και συνδυάζεται φυσικά με τους ευρύτερους ελέγχους μόνο για ανάγνωση στο έλεγχος κινδύνων ασφαλείας PDF με το PDFium Component και με εργασίες συμμόρφωσης μορφοτύπου όπως η επικύρωση εγγράφων PDF/X έτοιμων για εκτύπωση. Μόλις μπορείτε να διαβάσετε και να ταξινομήσετε μια υπογραφή, το επόμενο βήμα είναι η παραγωγή μίας: το δεύτερο άρθρο αυτής της σειράς καλύπτει την υπογραφή PDF με PAdES B-B, και το τρίτο επεκτείνει αυτό σε αξιόπιστες χρονοσημάνσεις και μακροπρόθεσμες υπογραφές B-LT και B-LTA. Ο έλεγχος υπογραφής μόνο για ανάγνωση και ο ταξινομητής ValidatePades που εμφανίζονται εδώ αποτελούν μέρος του PDFium Component για Delphi, C++Builder και Lazarus